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

2673 lines
80 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "UbaSessionClient.h"
#include "UbaApplicationRules.h"
#include "UbaConfig.h"
#include "UbaNetworkClient.h"
#include "UbaNetworkMessage.h"
#include "UbaProcess.h"
#include "UbaProcessStartInfoHolder.h"
#include "UbaProtocol.h"
#include "UbaStorage.h"
#if PLATFORM_WINDOWS
#include <winerror.h>
#include <tlhelp32.h>
#endif
namespace uba
{
void SessionClientCreateInfo::Apply(const Config& config)
{
SessionCreateInfo::Apply(config);
if (const ConfigTable* table = config.GetTable(TC("Session")))
{
table->GetValueAsBool(useDependencyCrawler, TC("UseDependencyCrawler"));
table->GetValueAsU32(pingTimeoutSecondsPrintCallstacks, TC("PingTimeoutSecondsPrintCallstacks"));
}
}
struct SessionClient::ModuleInfo
{
ModuleInfo(const tchar* n, const CasKey& c, u32 a) : name(n), casKey(c), attributes(a), done(true) {}
TString name;
CasKey casKey;
u32 attributes;
Event done;
};
SessionClient::SessionClient(const SessionClientCreateInfo& info)
: Session(info, TC("UbaSessionClient"), true, info.client)
, m_client(info.client)
, m_name(info.name.data)
, m_terminationTime(~0ull)
, m_waitToSendEvent(false)
, m_loop(true)
, m_allowSpawn(true)
{
m_maxProcessCount = info.maxProcessCount;
m_dedicated = info.dedicated;
m_useStorage = info.useStorage;
m_downloadDetoursLib = info.downloadDetoursLib;
m_defaultPriorityClass = info.defaultPriorityClass;
m_maxIdleSeconds = info.maxIdleSeconds;
m_osVersion = info.osVersion;
m_disableCustomAllocator = info.disableCustomAllocator;
m_useBinariesAsVersion = info.useBinariesAsVersion;
m_memWaitLoadPercent = info.memWaitLoadPercent;
m_memKillLoadPercent = info.memKillLoadPercent;
m_processFinished = info.processFinished;
#if PLATFORM_WINDOWS || PLATFORM_MAC // Linux will crash with print all callstacks
m_pingTimeoutSecondsPrintCallstacks = info.pingTimeoutSecondsPrintCallstacks;
#endif
m_useDependencyCrawler = info.useDependencyCrawler;
m_processIdCounter = ~0u / 2; // We set this value to a very high value.. because it will be used by child processes and we don't want id from server and child process id to collide
if (m_name.IsEmpty())
GetComputerNameW(m_name);
m_processWorkingDir.Append(m_rootDir).Append(TCV("empty"));// + TC("empty\\");
m_storage.CreateDirectory(m_processWorkingDir.data);
m_processWorkingDir.EnsureEndsWithSlash();
if (info.killRandom)
{
Guid g;
CreateGuid(g);
m_killRandomIndex = 10 + g.data1 % 30;
}
m_nameToHashTableMem.Init(NameToHashMemSize);
Create(info);
if (m_useDependencyCrawler)
m_dependencyCrawler.Init([this](const StringView& fileName, u32& outAttr) // FileExists
{
return Exists(fileName, outAttr);
},
[this](const StringView& path, const DependencyCrawler::FileFunc& fileFunc) // TraverseFilesFunc
{
u32 tableOffset = 0;
if (!EntryExists(path, tableOffset))
return;
if ((tableOffset & 0x80000000) == 0)
return;
tableOffset &= ~0x80000000;
// This is not entirely correct since files could be added while uba is running.. but since this is only used for includes that
// we know are created before uba started we're good.. and there should be no new files added to those dirs causing table offset to be chained
BinaryReader reader(m_directoryTableMem, tableOffset, m_directoryTableMemPos);
while (true)
{
u32 prevTableOffset = u32(reader.Read7BitEncoded());
if (!prevTableOffset)
break;
reader.SetPosition(prevTableOffset);
}
u32 dirAttr = reader.ReadFileAttributes();
if (!dirAttr)
return;
reader.ReadVolumeSerial();
reader.ReadFileIndex();
u64 itemCount = reader.Read7BitEncoded();
while (itemCount--)
{
StringBuffer<> fileName;
reader.ReadString(fileName);
if (CaseInsensitiveFs)
fileName.MakeLower();
u32 attr = reader.ReadFileAttributes();
bool isDir = IsDirectory(attr);
fileFunc(fileName, isDir);
reader.ReadVolumeSerial();
reader.ReadFileIndex();
if (isDir)
continue;
reader.ReadFileTime();
reader.ReadFileSize();
}
});
}
SessionClient::~SessionClient()
{
Stop();
}
bool SessionClient::Start()
{
m_client.RegisterOnDisconnected([this]()
{
m_loop = false;
});
m_client.RegisterOnConnected([this]()
{
Connect();
});
return true;
}
void SessionClient::Stop(bool wait)
{
m_loop = false;
m_waitToSendEvent.Set();
if (wait)
m_loopThread.Wait();
}
bool SessionClient::Wait(u32 milliseconds, Event* wakeupEvent)
{
return m_loopThread.Wait(milliseconds, wakeupEvent);
}
void SessionClient::SetIsTerminating(const tchar* reason, u64 delayMs)
{
m_terminationTime = GetTime() + MsToTime(delayMs);
m_terminationReason = reason;
SendNotification(ToView(reason));
}
void SessionClient::SetMaxProcessCount(u32 count)
{
m_maxProcessCount = count;
}
void SessionClient::SetAllowSpawn(bool allow)
{
m_allowSpawn = allow;
}
u64 SessionClient::GetBestPing()
{
return m_bestPing;
}
u32 SessionClient::GetMaxProcessCount()
{
return m_maxProcessCount;
}
bool SessionClient::Exists(const StringView& path, u32& outAttributes)
{
u32 tableOffset = 0;
if (!EntryExists(path, tableOffset))
return false;
outAttributes = m_directoryTable.GetAttributes(tableOffset);
return outAttributes != 0;
}
bool SessionClient::RetrieveCasFile(CasKey& outNewKey, u64& outSize, const CasKey& casKey, const tchar* hint, bool storeUncompressed, bool allowProxy)
{
//StringBuffer<> workName;
//u32 len = TStrlen(hint);
//workName.Append(TCV("RC:")).Append(len > 30 ? hint + (len - 30) : hint);
//TrackWorkScope tws(m_client, workName.data);
TimerScope s(m_stats.storageRetrieve);
CasKey tempKey = casKey;
if (storeUncompressed)
tempKey = AsCompressed(casKey, false);
//else
// storeUncompressed = !IsCompressed(casKey);
Storage::RetrieveResult result;
bool res = m_storage.RetrieveCasFile(result, tempKey, hint, nullptr, 1, allowProxy);
outNewKey = result.casKey;
outSize = result.size;
return res;
}
bool SessionClient::GetCasKeyForFile(CasKey& out, u32 processId, const StringView& fileName, const StringKey& fileNameKey)
{
TimerScope waitTimer(Stats().waitGetFileMsg);
SCOPED_FUTEX(m_nameToHashLookupLock, lock);
auto insres = m_nameToHashLookup.try_emplace(fileNameKey);
HashRec& rec = insres.first->second;
lock.Leave();
SCOPED_FUTEX(rec.lock, lock2);
if (rec.key == CasKeyZero)//!rec.serverTime)
{
waitTimer.Cancel();
// These will never succeed
if (fileName.StartsWith(m_sessionBinDir.data) || fileName.StartsWith(TC("c:\\noenvironment")) || fileName.StartsWith(m_processWorkingDir.data))
{
out = CasKeyZero;
return true;
}
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetFileFromServer, writer);
writer.WriteU32(processId);
writer.WriteString(fileName);
writer.WriteStringKey(fileNameKey);
StackBinaryReader<128> reader;
if (!msg.Send(reader, Stats().getFileMsg))
return false;
rec.key = reader.ReadCasKey();
if (rec.key != CasKeyZero)
rec.serverTime = reader.ReadU64();
}
out = rec.key;
return true;
}
bool SessionClient::EnsureBinaryFile(StringBufferBase& out, StringBufferBase& outVirtual, u32 processId, StringView fileName, const StringKey& fileNameKey, StringView applicationDir, StringView workingDir, const u8* loaderPaths, u32 loaderPathsSize)
{
CasKey casKey;
u32 fileAttributes = DefaultAttributes(); // TODO: This is wrong.. need to retrieve from server if this is executable or not
bool isAbsolute = IsAbsolutePath(fileName.data);
if (isAbsolute)
{
UBA_ASSERT(fileNameKey != StringKeyZero);
if (!GetCasKeyForFile(casKey, processId, fileName, fileNameKey))
return false;
// This needs to be absolute virtual path (the path on the host).. don't remember why this was written like this. Changed to just use fileName (needed for shadercompilerworker)
//const tchar* lastSlash = TStrrchr(fileName.data, PathSeparator);
//outVirtual.Append(lastSlash + 1);
outVirtual.Append(fileName);
}
else
{
UBA_ASSERT(fileNameKey == StringKeyZero);
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_EnsureBinaryFile, writer);
writer.WriteBool(IsRunningArm());
writer.WriteString(fileName);
writer.WriteStringKey(fileNameKey);
writer.WriteString(applicationDir);
writer.WriteString(workingDir);
if (loaderPathsSize)
writer.WriteBytes(loaderPaths, loaderPathsSize);
StackBinaryReader<1024> reader;
if (!msg.Send(reader, Stats().getBinaryMsg))
return false;
casKey = reader.ReadCasKey();
reader.ReadString(outVirtual);
}
if (casKey == CasKeyZero)
{
out.Append(fileName);
return true;
}
bool storeUncompressed = true;
CasKey newKey;
u64 fileSize;
if (!RetrieveCasFile(newKey, fileSize, casKey, outVirtual.data, storeUncompressed))
UBA_ASSERTF(false, TC("Casfile not found for %s using %s"), outVirtual.data, CasKeyString(casKey).str);
StringBuffer<> destFile;
if (isAbsolute || fileName.Contains(TC(".."))) // This is not beautiful, but we need to keep some dlls in the sub folder (for cl.exe etc)
destFile.AppendFileName(fileName.data);
else
destFile.Append(fileName);
StringBuffer<> applicationDirLower;
applicationDirLower.Append(applicationDir).MakeLower();
KeyToString keyStr(ToStringKey(applicationDirLower));
return WriteBinFile(out, destFile, newKey, keyStr, fileAttributes);
}
bool SessionClient::PrepareProcess(ProcessImpl& process, bool isChild, StringBufferBase& outRealApplication, const tchar*& outRealWorkingDir)
{
ProcessStartInfoHolder& startInfo = process.m_startInfo;
outRealWorkingDir = m_processWorkingDir.data;
if (StartsWith(startInfo.application, TC("ubacopy")))
return true;
#if PLATFORM_LINUX
//if (EndsWith(startInfo.application, TStrlen(startInfo.application), "/sh")) // For some reason the local shell application on the helpers hang (need to investigate aws shell application)
// return true;
#elif PLATFORM_WINDOWS
if (ToView(startInfo.application).EndsWith(TCV("system32\\cmd.exe"))) // We want to use local cmd.exe.. wine does not support kernel calls called by windows 11's cmd.exe for example
return true;
#endif
outRealApplication.Clear();
const tchar* application = startInfo.application;
UBA_ASSERT(application && *application);
bool isAbsolute = IsAbsolutePath(startInfo.application);
SCOPED_FUTEX(m_handledApplicationEnvironmentsLock, environmentslock);
auto insres = m_handledApplicationEnvironments.try_emplace(application);
environmentslock.Leave();
ApplicationEnvironment& appEnv = insres.first->second;
SCOPED_FUTEX(appEnv.lock, lock);
if (!appEnv.realApplication.empty())
{
outRealApplication.Append(appEnv.realApplication);
if (!isAbsolute)
{
startInfo.applicationStr = appEnv.virtualApplication;
startInfo.application = startInfo.applicationStr.c_str();
}
return true;
}
List<ModuleInfo> modules;
if (!ReadModules(modules, 0, application))
return false;
StringBuffer<MaxPath> applicationDir;
applicationDir.AppendDir(application);
KeyToString keyStr(ToStringKeyLower(applicationDir));
Atomic<bool> success = true;
m_client.ParallelFor(u32(modules.size()), modules, [&](const WorkContext&, auto& it)
{
auto& m = *it;
TrackWorkScope tws(m_client, AsView(TC("FetchModule")), ColorWork);
tws.AddHint(m.name);
auto g = MakeGuard([&]() { m.done.Set(); });
CasKey newCasKey;
bool storeUncompressed = true;
u64 fileSize;
const tchar* moduleName = m.name.c_str();
if (!RetrieveCasFile(newCasKey, fileSize, m.casKey, moduleName, storeUncompressed))
{
m_logger.Error(TC("Casfile not found for %s (%s)"), moduleName, CasKeyString(m.casKey).str);
success = false;
return;
}
if (const tchar* lastSeparator = TStrrchr(moduleName, PathSeparator))
moduleName = lastSeparator + 1;
StringBuffer<MaxPath> temp;
if (!WriteBinFile(temp, ToView(moduleName), newCasKey, keyStr, m.attributes))
success = false;
}, AsView(TC("FetchModule")), true);
if (!success)
return false;
outRealApplication.Append(m_sessionBinDir).Append(keyStr).Append(PathSeparator).AppendFileName(application);
appEnv.realApplication = outRealApplication.data;
if (!isAbsolute)
{
appEnv.virtualApplication = modules.front().name;
startInfo.applicationStr = appEnv.virtualApplication;
startInfo.application = startInfo.applicationStr.c_str();
}
return true;
}
bool SessionClient::ReadModules(List<ModuleInfo>& outModules, u32 processId, const tchar* application)
{
TrackWorkScope tws(m_client, AsView(TC("ReadModules")), ColorWork);
StackBinaryReader<16*1024> reader;
{
StackBinaryWriter<256> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetApplication, writer);
writer.WriteU32(processId);
writer.WriteString(application);
if (!msg.Send(reader, m_stats.getApplicationMsg))
return false;
}
u32 serverSystemPathLen = reader.ReadU32();
u32 moduleCount = reader.ReadU32();
if (moduleCount == 0)
return m_logger.Error(TC("Application %s not found"), application);
while (moduleCount--)
{
StringBuffer<> moduleFile;
reader.ReadString(moduleFile);
u32 fileAttributes = reader.ReadU32();
bool isSystem = reader.ReadBool();
CasKey casKey = reader.ReadCasKey();
if (casKey == CasKeyZero)
return m_logger.Error(TC("Bad CasKey for %s (%s)"), moduleFile.data, CasKeyString(casKey).str);
#if PLATFORM_MAC
u32 minOsVersion = reader.ReadU32();
if (m_osVersion && m_osVersion < minOsVersion)
return m_logger.Error(TC("%s has min os version %u but current os is %u"), moduleFile.data, minOsVersion, m_osVersion);
#endif
if (isSystem)
{
StringBuffer<> localSystemModule;
localSystemModule.Append(m_systemPath).Append(moduleFile.data + serverSystemPathLen);
if (FileExists(m_logger, localSystemModule.data) && !localSystemModule.EndsWith(TCV(".exe")))
continue;
moduleFile.Clear().Append(localSystemModule);
}
outModules.emplace_back(moduleFile.data, casKey, fileAttributes);
}
return true;
}
void* SessionClient::GetProcessEnvironmentVariables()
{
UBA_ASSERT(!m_environmentVariables.empty());
return m_environmentVariables.data();
}
bool SessionClient::WriteBinFile(StringBufferBase& out, const StringView& binaryName, const CasKey& casKey, const KeyToString& applicationDir, u32 fileAttributes)
{
UBA_ASSERT(fileAttributes);
out.Append(m_sessionBinDir);
out.Append(applicationDir).Append(PathSeparator);
StringBuffer<> lower;
lower.Append(applicationDir).Append(PathSeparator).Append(binaryName);
lower.MakeLower();
SCOPED_FUTEX(m_binFileLock, lock);
auto insres = m_writtenBinFiles.try_emplace(lower.data, casKey);
if (!insres.second)
{
out.Append(binaryName);
if (insres.first->second != casKey)
return m_logger.Error(TC("Writing same binary file %s multiple times but with different data! (Current: %s Previous: %s)"), out.data, CasKeyString(casKey).str, CasKeyString(insres.first->second).str);
return true;
}
m_storage.CreateDirectory(out.data);
out.Append(binaryName);
//if (GetFileAttributesW(out.data) != INVALID_FILE_ATTRIBUTES)
// return true;
if (TStrchr(binaryName.data, PathSeparator))
{
StringBuffer<> binaryDir;
binaryDir.AppendDir(out);
if (!m_storage.CreateDirectory(binaryDir.data))
return false;
}
// Special hack to prevent two identical dlls from pointing to the same file (dlls are deduplicated if they are the same file in the backend)
bool allowHardlink = !binaryName.GetFileName().StartsWith(TC("c2"));
constexpr bool writeCompressed = false;
constexpr bool isTemp = true;
return m_storage.CopyOrLink(casKey, out.data, fileAttributes, writeCompressed, {}, isTemp, allowHardlink);
}
bool SessionClient::ProcessThreadStart(ProcessImpl& process)
{
if (!Session::ProcessThreadStart(process))
return false;
#if PLATFORM_LINUX
// TODO. Linux linker has lots of stuff going on. it is a shell script deleting, renaming, overwriting same file causing confusion with child processes
// Since directory table is not updated from server between child processes we end up with not up-to-date table.. therefore we flush everything in between
if (auto parent = process.m_parentProcess)
{
auto& si = parent->m_startInfo;
if (ToView(si.application).EndsWith(TCV("/sh")))
{
FlushWrittenFiles(*parent);
StackBinaryReader<SendMaxSize> reader;
SendUpdateDirectoryTable(reader);
}
}
#endif
if (RootsHandle rootsHandle = process.GetStartInfo().rootsHandle)
if (!SendRootsHandle(rootsHandle))
return false;
if (!process.m_parentProcess && m_useDependencyCrawler)
RunDependencyCrawler(process);
return true;
}
bool SessionClient::CreateFileForRead(CreateFileResponse& out, TrackWorkScope& tws, const StringView& fileName, const StringKey& fileNameKey, ProcessImpl& process, const ApplicationRules& rules)
{
CasKey casKey;
if (!GetCasKeyForFile(casKey, process.GetId(), fileName, fileNameKey))
return false;
// Not finding a file is a valid path. Some applications try with a path and if fails try another path
if (casKey == CasKeyZero)
{
//m_logger.Warning(TC("No casfile found for %s"), fileName.data);
out.directoryTableSize = GetDirectoryTableSize();
out.mappedFileTableSize = GetFileMappingSize();
out.fileName.Append(fileName);
return true;
}
// Code for doing retry if failing to decompress casfile. We've seen cases of corrupt cas files on clients
bool shouldRetry = true;
FileMappingEntry* retryEntry = nullptr;
auto retryEntryGuard = MakeGuard([&]() { if (retryEntry) retryEntry->lock.Leave(); });
while (true)
{
StringBuffer<> newName;
bool isDir = casKey == CasKeyIsDirectory;
u64 fileSize = InvalidValue;
CasKey newCasKey;
#ifdef __clang_analyzer__
newCasKey = {};
#endif
u32 memoryMapAlignment = 0;
if (m_allowMemoryMaps)
{
memoryMapAlignment = GetMemoryMapAlignment(fileName);
if (!memoryMapAlignment && !m_useStorage)
memoryMapAlignment = 64 * 1024;
}
if (isDir)
{
newName.Append(TCV("$d"));
}
else if (casKey != CasKeyZero)
{
if (m_useStorage || memoryMapAlignment == 0)
{
bool storeUncompressed = memoryMapAlignment == 0;
bool allowProxy = rules.AllowStorageProxy(fileName);
if (!RetrieveCasFile(newCasKey, fileSize, casKey, fileName.data, storeUncompressed, allowProxy))
return m_logger.Error(TC("Error retrieving cas entry %s (%s)"), CasKeyString(casKey).str, fileName.data);
if (!m_storage.GetCasFileName(newName, newCasKey))
return false;
}
else
{
StorageStats& stats = m_storage.Stats();
TimerScope ts(stats.ensureCas);
SCOPED_FUTEX(m_fileMappingTableLookupLock, lookupLock);
auto insres = m_fileMappingTableLookup.try_emplace(fileNameKey);
FileMappingEntry& entry = insres.first->second;
lookupLock.Leave();
SCOPED_FUTEX(entry.lock, entryCs);
ts.Leave();
if (entry.handled)
{
if (!entry.success)
return false;
}
else
{
TimerScope s(m_stats.storageRetrieve);
casKey = AsCompressed(casKey, false);
entry.handled = true;
Storage::RetrieveResult result;
bool allowProxy = rules.AllowStorageProxy(fileName);
if (!m_storage.RetrieveCasFile(result, casKey, fileName.data, &m_fileMappingBuffer, memoryMapAlignment, allowProxy))
return m_logger.Error(TC("Error retrieving cas entry %s (%s)"), CasKeyString(casKey).str, fileName.data);
entry.success = true;
entry.size = result.size;
entry.mapping = result.view.handle;
entry.mappingOffset = result.view.offset;
}
fileSize = entry.size;
if (entry.mapping.IsValid())
Storage::GetMappingString(newName, entry.mapping, entry.mappingOffset);
else
newName.Append(entry.isDir ? TC("$d") : TC("$f"));
}
}
UBA_ASSERTF(!newName.IsEmpty(), TC("No casfile available for %s using %s"), fileName.data, CasKeyString(casKey).str);
if (newName[0] != '^')
{
TrackHintScope ths(tws, AsView(TC("CreateMemoryMap")));
if (!isDir && memoryMapAlignment)
{
if (retryEntry)
retryEntryGuard.Execute();
MemoryMap map;
if (!CreateMemoryMapFromFile(map, fileNameKey, newName.data, IsCompressed(newCasKey), memoryMapAlignment, fileName.data, nullptr, false))
{
if (!shouldRetry)
return false;
shouldRetry = false;
// We need to take a lock around the file map entry since there might be another thread also wanting to map this
{
SCOPED_FUTEX(m_fileMappingTableLookupLock, lookupLock);
retryEntry = &m_fileMappingTableLookup.try_emplace(fileNameKey).first->second;
lookupLock.Leave();
retryEntry->lock.Enter();
retryEntry->handled = false;
}
if (!m_storage.ReportBadCasFile(newCasKey))
return false;
continue;
}
fileSize = map.size;
newName.Clear().Append(map.name);
}
else if (!rules.IsRarelyRead(fileName))
{
AddFileMapping(fileNameKey, fileName.data, newName.data, fileSize);
}
}
out.directoryTableSize = GetDirectoryTableSize();
out.mappedFileTableSize = GetFileMappingSize();
out.fileName.Append(newName);
out.size = fileSize;
return true;
}
}
bool SessionClient::SendFiles(ProcessImpl& process, Timer& sendFiles)
{
StorageStatsScope storageStatsScope(process.m_storageStats);
for (auto& pair : process.m_shared.writtenFiles)
{
WrittenFile& file = pair.second;
TimerScope timer(sendFiles);
#ifdef _DEBUG
if (!pair.second.mappingHandle.IsValid())
m_logger.Warning(TC("%s is not using file mapping"), pair.first.c_str());
#endif
bool keepMappingInMemory = IsWindows && !IsRarelyReadAfterWritten(process, file.name);
bool compressed = process.m_startInfo.rules->SendFileCompressedFromClient(file.name);
if (!SendFile(file, process.GetId(), keepMappingInMemory, compressed))
return false;
}
return true;
}
bool SessionClient::SendFile(WrittenFile& file, u32 processId, bool keepMappingInMemory, bool compressed)
{
CasKey casKey;
{
TimerScope ts(m_stats.storageSend);
if (!m_storage.StoreCasFileClient(casKey, file.key, file.backedName.c_str(), file.mappingHandle, 0, file.mappingWritten, file.name.c_str(), keepMappingInMemory, compressed))
return false;
}
if (casKey == CasKeyZero)
return m_logger.Error(TC("Failed to store cas on server for local file %s (size %llu destination %s)"), file.backedName.c_str(), file.mappingWritten, file.name.c_str());
CloseFileMapping(m_logger, file.mappingHandle, file.backedName.c_str());
file.mappingHandle = {};
StackBinaryReader<128> reader;
{
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_SendFileToServer, writer);
writer.WriteU32(processId);
writer.WriteString(file.name);
writer.WriteStringKey(file.key);
writer.WriteU32(file.attributes);
writer.WriteCasKey(casKey);
if (!msg.Send(reader, Stats().sendFileMsg))
return m_logger.Error(TC("Failed to send file %s to server"), file.backedName.c_str());
}
#if UBA_DEBUG_LOGGER
m_debugLogger->Info(TC("SENDFILE %s\n"), file.name.c_str());
#endif
if (!reader.ReadBool())
return m_logger.Error(TC("Server failed to copy cas %s to %s (local source %s)"), CasKeyString(casKey).str, file.name.c_str(), file.backedName.c_str());
return true;
}
bool SessionClient::DeleteFile(DeleteFileResponse& out, const DeleteFileMessage& msg)
{
// TODO: Deleting output files should also delete them on disk (for now they will leak until process shutdown)
RemoveWrittenFile(msg.process, msg.fileNameKey);
bool sendDelete = true;
if (msg.closeId != 0)
{
UBA_ASSERTF(false, TC("This has not been tested properly"));
SCOPED_FUTEX(m_activeFilesLock, lock);
sendDelete = m_activeFiles.erase(msg.closeId) == 0;
}
{
SCOPED_FUTEX(m_outputFilesLock, lock);
sendDelete = m_outputFiles.erase(msg.fileName.data) == 0 && sendDelete;
}
bool isTemp = StartsWith(msg.fileName.data, m_tempPath.data);
if (isTemp)
sendDelete = false;
if (!sendDelete)
{
if (!m_allowMemoryMaps && isTemp)
{
out.result = uba::DeleteFileW(msg.fileName.data);
out.errorCode = GetLastError();
return true;
}
out.result = true;
out.errorCode = ERROR_SUCCESS;
return true;
}
// TODO: Cache this if it becomes noisy
StackBinaryWriter<1024> writer;
NetworkMessage networkMsg(m_client, ServiceId, SessionMessageType_DeleteFile, writer);
writer.WriteStringKey(msg.fileNameKey);
writer.WriteString(msg.fileName);
StackBinaryReader<SendMaxSize> reader;
if (!networkMsg.Send(reader, Stats().deleteFileMsg))
return false;
out.result = reader.ReadBool();
out.errorCode = reader.ReadU32();
if (out.result)
if (!SendUpdateDirectoryTable(reader.Reset()))
return false;
out.directoryTableSize = GetDirectoryTableSize();
return true;
}
bool SessionClient::CopyFile(CopyFileResponse& out, const CopyFileMessage& msg)
{
SCOPED_FUTEX(m_outputFilesLock, lock);
auto findIt = m_outputFiles.find(msg.fromName.data);
if (findIt == m_outputFiles.end())
{
lock.Leave();
StackBinaryWriter<1024> writer;
NetworkMessage networkMsg(m_client, ServiceId, SessionMessageType_CopyFile, writer);
writer.WriteStringKey(msg.fromKey);
writer.WriteString(msg.fromName);
writer.WriteStringKey(msg.toKey);
writer.WriteString(msg.toName);
StackBinaryReader<SendMaxSize> reader;
if (!networkMsg.Send(reader, Stats().copyFileMsg))
return false;
out.fromName.Append(msg.fromName);
out.toName.Append(msg.toName);
out.closeId = ~0u;
out.errorCode = reader.ReadU32();
if (!out.errorCode)
if (!SendUpdateDirectoryTable(reader.Reset()))
return false;
out.directoryTableSize = GetDirectoryTableSize();
return true;
}
lock.Leave();
out.fromName.Append(findIt->second);
CreateFileMessage writeMsg { msg.process };
writeMsg.fileName.Append(msg.toName.data);
writeMsg.fileNameKey = msg.toKey;
writeMsg.access = FileAccess_Write;
CreateFileResponse writeOut;
if (!CreateFile(writeOut, writeMsg))
return false;
out.toName.Append(writeOut.fileName);
out.closeId = writeOut.closeId;
return true;
}
bool SessionClient::MoveFile(MoveFileResponse& out, const MoveFileMessage& msg)
{
const tchar* fromName = msg.fromName.data;
const tchar* toName = msg.toName.data;
auto& process = msg.process;
{
SCOPED_FUTEX(process.m_shared.writtenFilesLock, lock);
auto& writtenFiles = process.m_shared.writtenFiles;
auto findIt = writtenFiles.find(msg.fromKey);
if (findIt != writtenFiles.end())
{
UBA_ASSERT(msg.toKey != StringKeyZero);
auto insres = writtenFiles.try_emplace(msg.toKey);
UBA_ASSERTF(insres.second, TC("Moving written file %s to other written file %s. (%s)"), fromName, toName, process.m_startInfo.description);
insres.first->second = findIt->second;
insres.first->second.key = msg.toKey;
insres.first->second.name = toName;
insres.first->second.owner = &process;
writtenFiles.erase(findIt);
}
else
{
// TODO: Need to tell server
}
}
bool sendMove = true;
{
SCOPED_FUTEX(m_outputFilesLock, lock);
auto findIt = m_outputFiles.find(fromName);
if (findIt != m_outputFiles.end())
{
auto insres = m_outputFiles.try_emplace(toName);
UBA_ASSERTF(insres.second, TC("Failed to add move destination file %s as output file because it is already added. (Moved from %s)"), toName, fromName);
insres.first->second = findIt->second;
m_outputFiles.erase(findIt);
sendMove = false;
}
}
if (!sendMove)
{
out.result = true;
out.errorCode = ERROR_SUCCESS;
return true;
}
// TODO: This should be done by server?
out.result = uba::MoveFileExW(fromName, toName, 0);
out.errorCode = GetLastError();
return true;
}
bool SessionClient::Chmod(ChmodResponse& out, const ChmodMessage& msg)
{
{
SCOPED_FUTEX(msg.process.m_shared.writtenFilesLock, lock);
auto& writtenFiles = msg.process.m_shared.writtenFiles;
auto findIt = writtenFiles.find(msg.fileNameKey);
if (findIt != writtenFiles.end())
{
bool executable = false;
#if !PLATFORM_WINDOWS
if (msg.fileMode & S_IXUSR)
executable = true;
#endif
findIt->second.attributes = DefaultAttributes(executable);
out.errorCode = 0;
return true;
}
}
UBA_ASSERTF(false, TC("Code path not implemented.. should likely send message to server"));
return true;
}
bool SessionClient::CreateDirectory(CreateDirectoryResponse& out, const CreateDirectoryMessage& msg)
{
// TODO: Cache this if it becomes noisy
StackBinaryWriter<1024> writer;
NetworkMessage networkMsg(m_client, ServiceId, SessionMessageType_CreateDirectory, writer);
writer.WriteString(msg.name);
StackBinaryReader<SendMaxSize> reader;
if (!networkMsg.Send(reader, Stats().createDirMsg))
return false;
out.result = reader.ReadBool();
out.errorCode = reader.ReadU32();
if (out.result || out.errorCode == ERROR_ALREADY_EXISTS) // Even if it exists it might be that this client session is not aware
if (!SendUpdateDirectoryTable(reader.Reset()))
return false;
out.directoryTableSize = GetDirectoryTableSize();
return true;
}
bool SessionClient::RemoveDirectory(RemoveDirectoryResponse& out, const RemoveDirectoryMessage& msg)
{
StackBinaryWriter<1024> writer;
NetworkMessage networkMsg(m_client, ServiceId, SessionMessageType_RemoveDirectory, writer);
writer.WriteString(msg.name);
StackBinaryReader<SendMaxSize> reader;
if (!networkMsg.Send(reader, Stats().deleteFileMsg)) // Wrong message
return false;
out.result = reader.ReadBool();
out.errorCode = reader.ReadU32();
if (out.result)
if (!SendUpdateDirectoryTable(reader.Reset()))
return false;
out.directoryTableSize = GetDirectoryTableSize();
return true;
}
bool SessionClient::GetFullFileName(GetFullFileNameResponse& out, const GetFullFileNameMessage& msg)
{
StringView workingDir(msg.process.m_startInfo.workingDirStr);
StringKeyHasher hasher;
hasher.UpdateNoCheck(msg.process.m_startInfo.applicationStr);
hasher.UpdateNoCheck(msg.fileName);
hasher.UpdateNoCheck(workingDir);
StringKey nameKey = ToStringKey(hasher);
SCOPED_FUTEX(m_nameToNameLookupLock, lock);
auto insres = m_nameToNameLookup.try_emplace(nameKey);
NameRec& rec = insres.first->second;
lock.Leave();
SCOPED_FUTEX(rec.lock, lock2);
if (rec.handled)
{
out.fileName.Append(rec.name.c_str());
out.virtualFileName.Append(rec.virtualName.c_str());
return true;
}
rec.handled = true;
StringBuffer<> appDir;
appDir.AppendDir(msg.process.m_startInfo.application);
if (!EnsureBinaryFile(out.fileName, out.virtualFileName, msg.process.m_id, msg.fileName, msg.fileNameKey, appDir, workingDir, msg.loaderPaths, msg.loaderPathsSize))
return false;
StringKey fileNameKey = msg.fileNameKey;
if (fileNameKey == StringKeyZero)
fileNameKey = CaseInsensitiveFs ? ToStringKeyLower(out.virtualFileName) : ToStringKey(out.virtualFileName);
rec.name = out.fileName.data;
rec.virtualName = out.virtualFileName.data;
out.mappedFileTableSize = AddFileMapping(fileNameKey, msg.fileName.data, out.fileName.data);
return true;
}
bool SessionClient::GetLongPathName(GetLongPathNameResponse& out, const GetLongPathNameMessage& msg)
{
StackBinaryWriter<1024> writer;
NetworkMessage networkMsg(m_client, ServiceId, SessionMessageType_GetLongPathName, writer);
writer.WriteString(msg.fileName);
StackBinaryReader<1024> reader;
if (!networkMsg.Send(reader, Stats().getLongNameMsg))
return false;
out.errorCode = reader.ReadU32();
reader.ReadString(out.fileName);
return true;
}
bool SessionClient::GetListDirectoryInfo(ListDirectoryResponse& out, const StringView& dirName, const StringKey& dirKey)
{
TrackWorkScope tws(m_client, AsView(TC("GetListDir")), ColorWork);
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_ListDirectory, writer);
writer.WriteU32(m_sessionId);
writer.WriteString(dirName);
writer.WriteStringKey(dirKey);
StackBinaryReader<SendMaxSize> reader;
if (!msg.Send(reader, Stats().listDirMsg))
return false;
u32 tableOffset = reader.ReadU32();
u32 oldTableSize = GetDirectoryTableSize();
if (!UpdateDirectoryTableFromServer(reader))
return false;
u32 newTableSize = GetDirectoryTableSize();
// Ask for a refresh of hashes straight away since they will likely be asked for by the process doing this query
if (oldTableSize != newTableSize)
m_waitToSendEvent.Set();
out.tableOffset = tableOffset;
out.tableSize = newTableSize;
return true;
}
bool SessionClient::WriteFilesToDisk(ProcessImpl& process, WrittenFile** files, u32 fileCount)
{
// Do nothing, we will send the data to the host when process is finished
return true;
}
struct SessionClient::ActiveUpdateDirectoryEntry
{
Event done;
u32 readPos = 0;
ActiveUpdateDirectoryEntry* prev = nullptr;
ActiveUpdateDirectoryEntry* next = nullptr;
bool success = true;
static bool Wait(SessionClient& client, ActiveUpdateDirectoryEntry*& first, ScopedFutex& lock, u32 readPos, const tchar* hint)
{
ActiveUpdateDirectoryEntry item;
item.next = first;
if (item.next)
item.next->prev = &item;
item.readPos = readPos;
first = &item;
item.done.Create(true);
lock.Leave();
bool res = item.done.IsSet(5*60*1000);
lock.Enter();
if (item.prev)
item.prev->next = item.next;
else
first = item.next;
if (item.next)
item.next->prev = item.prev;
if (res)
return item.success;
u32 activeCount = 0;
for (auto i = first; i; i = i->next)
++activeCount;
return client.m_logger.Error(TC("Timed out after 5 minutes waiting for update directory message to reach read position %u (%u active in %s wait)"), readPos, activeCount, hint);
}
static void UpdateReadPosMatching(ActiveUpdateDirectoryEntry*& first, u32 readPos)
{
for (auto i = first; i; i = i->next)
{
if (i->readPos != readPos)
continue;
i->done.Set();
break;
}
}
static void UpdateReadPosLessOrEqual(ActiveUpdateDirectoryEntry*& first, u32 readPos)
{
for (auto i = first; i; i = i->next)
if (i->readPos <= readPos)
i->done.Set();
}
static void UpdateError(ActiveUpdateDirectoryEntry*& first)
{
for (auto i = first; i; i = i->next)
{
i->success = false;
i->done.Set();
}
}
};
bool SessionClient::UpdateDirectoryTableFromServer(StackBinaryReader<SendMaxSize>& reader)
{
auto& dirTable = m_directoryTable;
auto updateMemorySizeAndSignal = [&]
{
SCOPED_WRITE_LOCK(dirTable.m_memoryLock, lock);
dirTable.m_memorySize = m_directoryTableMemPos;
lock.Leave();
ActiveUpdateDirectoryEntry::UpdateReadPosLessOrEqual(m_firstEmptyWait, m_directoryTableMemPos);
return true;
};
u32 lastWriteEnd = ~0u;
while (true)
{
u32 readPos = reader.ReadU32();
u8* pos = m_directoryTableMem + readPos;
u32 toRead = u32(reader.GetLeft());
SCOPED_FUTEX(m_directoryTableLock, lock);
if (m_directoryTableError)
return false;
EnsureDirectoryTableMemory(readPos + toRead);
if (toRead == 0)
{
// We wrote to lastWriteEnd and now we got an empty message where readPos is the same..
// This means that it was a good cut-off and we can increase m_memorySize
// If m_directoryTableMemPos is different it means that we have another thread going on that will update things a little bit later.
if (lastWriteEnd == readPos && lastWriteEnd == m_directoryTableMemPos)
return updateMemorySizeAndSignal();
// We might share this position with others
if (dirTable.m_memorySize < readPos)
if (!ActiveUpdateDirectoryEntry::Wait(*this, m_firstEmptyWait, lock, readPos, TC("empty")))
return false;
return true;
}
reader.ReadBytes(pos, toRead);
// Wait until all data before readPos has been read
if (readPos != m_directoryTableMemPos)
if (!ActiveUpdateDirectoryEntry::Wait(*this, m_firstReadWait, lock, readPos, TC("read")))
return false;
m_directoryTableMemPos += toRead;
// Find potential waiter waiting for this exact size and wake it up
ActiveUpdateDirectoryEntry::UpdateReadPosMatching(m_firstReadWait, m_directoryTableMemPos);
// If there is space left in the message it means that we caught up with the directory table server side..
// And we will stop asking for more data.
// Note, we can only set m_memorySize when getting messages that reads less than capacity since we don't know if we reached a good position in the directory table
if (reader.GetPosition() < m_client.GetMessageMaxSize() - m_client.GetMessageReceiveHeaderSize())
return updateMemorySizeAndSignal();
lastWriteEnd = m_directoryTableMemPos;
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetDirectoriesFromServer, writer);
writer.WriteU32(m_sessionId);
if (msg.Send(reader.Reset(), Stats().getDirsMsg))
continue;
// Let's signal waiters to exit faster since we will not get out of this situation (most likely a disconnect)
m_directoryTableError = true;
ActiveUpdateDirectoryEntry::UpdateError(m_firstReadWait);
ActiveUpdateDirectoryEntry::UpdateError(m_firstEmptyWait);
return false;
}
}
bool SessionClient::UpdateNameToHashTableFromServer(StackBinaryReader<SendMaxSize>& reader)
{
u32 serverTableSize = 0;
bool isFirst = true;
u32 readStartPos = u32(m_nameToHashTableMem.writtenSize);
u32 localTableSize = readStartPos;
u64 serverTime = 0;
while (true)
{
if (isFirst)
{
serverTableSize = reader.ReadU32();
isFirst = false;
}
else
{
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetNameToHashFromServer, writer);
writer.WriteU32(serverTableSize);
writer.WriteU32(localTableSize);
if (!msg.Send(reader.Reset(), Stats().getHashesMsg))
return false;
}
serverTime = reader.ReadU64();
u8* pos = m_nameToHashTableMem.memory + localTableSize;
u32 left = u32(reader.GetLeft());
u32 toRead = serverTableSize - localTableSize;
bool needMore = left < toRead;
if (needMore)
toRead = left;
m_nameToHashTableMem.AllocateNoLock(toRead, 1, TC("NameToHashTable"));
reader.ReadBytes(pos, toRead);
localTableSize += toRead;
if (!needMore)
break;
}
u32 addCount = 0;
BinaryReader r(m_nameToHashTableMem.memory, readStartPos, NameToHashMemSize);
SCOPED_FUTEX(m_nameToHashLookupLock, lock);
while (r.GetPosition() < localTableSize)
{
StringKey name = r.ReadStringKey();
CasKey hash = r.ReadCasKey();
HashRec& rec = m_nameToHashLookup[name];
SCOPED_FUTEX(rec.lock, lock2);
if (serverTime < rec.serverTime)
continue;
rec.key = hash;
rec.serverTime = serverTime;
++addCount;
}
return true;
}
void SessionClient::Connect()
{
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_Connect, writer);
writer.WriteString(m_name.data);
writer.WriteU32(SessionNetworkVersion);
writer.WriteBool(IsRunningArm());
CasKey keys[2];
if (m_useBinariesAsVersion)
{
StringBuffer<> dir;
GetDirectoryOfCurrentModule(m_logger, dir);
u64 dirCount = dir.count;
dir.Append(PathSeparator).Append(UBA_AGENT_EXECUTABLE);
m_storage.CalculateCasKey(keys[0], dir.data);
dir.Resize(dirCount).Append(PathSeparator).Append(UBA_DETOURS_LIBRARY);
m_storage.CalculateCasKey(keys[1], dir.data);
}
writer.WriteCasKey(keys[0]);
writer.WriteCasKey(keys[1]);
writer.WriteU32(m_maxProcessCount);
writer.WriteBool(m_dedicated);
StringBuffer<> info;
GetSystemInfo(info);
writer.WriteString(info);
u64 memAvail;
u64 memTotal;
GetMemoryInfo(memAvail, memTotal);
float cpuLoad = UpdateCpuLoad();
m_cpuUsage = cpuLoad;
m_memAvail = memAvail;
m_memTotal = memTotal;
writer.WriteU64(memAvail);
writer.WriteU64(memTotal);
writer.WriteU32(*(u32*)&cpuLoad);
StackBinaryReader<SendMaxSize> reader;
if (!msg.Send(reader, m_stats.connectMsg))
return;
if (!reader.ReadBool())
{
StringBuffer<> str;
reader.ReadString(str);
m_logger.Error(str.data);
CasKey exeKey = reader.ReadCasKey();
CasKey dllKey = reader.ReadCasKey();
m_client.InvokeVersionMismatch(exeKey, dllKey);
return;
}
u32 isArm = IsRunningArm();
CasKey detoursBinaryKey[2];
detoursBinaryKey[0] = reader.ReadCasKey();
if (isArm)
detoursBinaryKey[1] = reader.ReadCasKey();
StringBuffer<> detoursFile[2];
if (m_downloadDetoursLib)
{
for (u32 i=0; i!=isArm+1; ++i)
{
{
TimerScope s(m_stats.storageRetrieve);
Storage::RetrieveResult result;
if (!m_storage.RetrieveCasFile(result, AsCompressed(detoursBinaryKey[i], false), UBA_DETOURS_LIBRARY))
return;
}
StringKey key;
key.a = i;
KeyToString dir(key);
if (!WriteBinFile(detoursFile[i], AsView(UBA_DETOURS_LIBRARY), detoursBinaryKey[i], dir, DefaultAttributes()))
return;
}
}
else
{
GetDirectoryOfCurrentModule(m_logger, detoursFile[isArm]);
detoursFile[isArm].EnsureEndsWithSlash().Append(UBA_DETOURS_LIBRARY);
}
for (u32 i=0; i!=isArm+1; ++i)
{
#if PLATFORM_WINDOWS
char dll[1024];
detoursFile[i].Parse(dll, sizeof_array(dll));
m_detoursLibrary[i] = dll;
#else
m_detoursLibrary[i] = detoursFile[i].data;
#endif
}
bool resetCas = reader.ReadBool();
if (resetCas)
m_storage.Reset();
m_sessionId = reader.ReadU32();
m_uiLanguage = reader.ReadU32();
m_storeIntermediateFilesCompressed = reader.ReadBool();
m_detailedTrace = reader.ReadBool();
m_shouldSendLogToServer = reader.ReadBool();
m_shouldSendTraceToServer = reader.ReadBool();
m_readIntermediateFilesCompressed = reader.ReadBool();
auto& serverName = detoursFile[0].Clear(); // reuse
reader.ReadString(serverName);
m_logger.Info(TC("Connected to server %s"), serverName.data);
if (m_shouldSendTraceToServer)
{
//if (m_detailedTrace)
{
m_client.SetWorkTracker(&m_trace);
}
StartTrace(nullptr, 256);
}
else
{
StartTraceThread(); // We still use trace thread for ping messages
}
BuildEnvironmentVariables(reader);
m_loopThread.Start([this]() { ThreadCreateProcessLoop(); return 0; }, TC("UbaCreateProc"));
}
void SessionClient::BuildEnvironmentVariables(BinaryReader& reader)
{
TString tempStr;
while (true)
{
tempStr = reader.ReadString();
if (tempStr.empty())
break;
m_environmentVariables.insert(m_environmentVariables.end(), tempStr.begin(), tempStr.end());
m_environmentVariables.push_back(0);
}
#if PLATFORM_WINDOWS
AddEnvironmentVariableNoLock(TC("TEMP"), m_tempPath.data);
AddEnvironmentVariableNoLock(TC("TMP"), m_tempPath.data);
#else
AddEnvironmentVariableNoLock(TC("TMPDIR"), m_tempPath.data);
#endif
StringBuffer<> v;
for (auto& var : m_localEnvironmentVariables)
if (GetEnvironmentVariableW(var, v.data, v.capacity))
AddEnvironmentVariableNoLock(var, v.data);
m_environmentVariables.push_back(0);
}
struct SessionClient::InternalProcessStartInfo : ProcessStartInfoHolder
{
u32 processId = 0;
};
bool SessionClient::SendProcessAvailable(Vector<InternalProcessStartInfo>& out, float availableWeight)
{
StackBinaryReader<SendMaxSize> reader;
{
TrackWorkScope tws(m_client, AsView(TC("RequestProcesses")), ColorWork);
StackBinaryWriter<32> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_ProcessAvailable, writer);
writer.WriteU32(m_sessionId);
writer.WriteU32(*(u32*)&availableWeight);
if (!msg.Send(reader, m_stats.procAvailableMsg))
{
if (m_loop)
m_logger.Error(TC("Failed to send ProcessAvailable message"));
return false;
}
}
while (true)
{
u32 processId = reader.ReadU32();
if (processId == 0)
break;
if (processId == SessionProcessAvailableResponse_Disconnect)
{
m_logger.Info(TC("Got disconnect request from host"));
return false;
}
if (processId == SessionProcessAvailableResponse_RemoteExecutionDisabled)
{
m_remoteExecutionEnabled = false;
break;
}
out.push_back({});
InternalProcessStartInfo& info = out.back();
info.processId = processId;
info.Read(reader);
}
u32 neededDirectoryTableSize = reader.ReadU32();
u32 neededHashTableSize = reader.ReadU32();
if (u32 knownInputsCount = reader.ReadU32())
{
while (knownInputsCount--)
{
CasKey knownInputKey = reader.ReadCasKey();
u32 mappingAlignment = reader.ReadU32();
bool allowProxy = reader.ReadBool();
bool storeUncompressed = !m_allowMemoryMaps || mappingAlignment == 0;
if (storeUncompressed)
knownInputKey = AsCompressed(knownInputKey, false);
m_client.AddWork([knownInputKey, allowProxy, this](const WorkContext&)
{
TrackWorkScope tws(m_client, StringBuffer<>().Appendf(TC("KnownInput")), ColorWork);
Storage::RetrieveResult result;
bool res = m_storage.RetrieveCasFile(result, knownInputKey, TC("KnownInput"), nullptr, 1, allowProxy);
(void)res;
}, 1, TC("KnownInput"));
}
}
if (!out.empty())
if (neededDirectoryTableSize > GetDirectoryTableSize())
if (!SendUpdateDirectoryTable(reader.Reset()))
return false;
// Always nice to update name-to-hash table since it can reduce number of messages while building.
u32 hashTableMemSize;
{
SCOPED_READ_LOCK(m_nameToHashMemLock, l);
hashTableMemSize = u32(m_nameToHashTableMem.writtenSize);
}
if (neededHashTableSize > hashTableMemSize)
if (!SendUpdateNameToHashTable(reader.Reset()))
return false;
return true;
}
void SessionClient::SendReturnProcess(u32 processId, const tchar* reason)
{
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_ProcessReturned, writer);
writer.WriteU32(processId);
writer.WriteString(reason);
StackBinaryReader<32> reader;
if (!msg.Send(reader, m_stats.procReturnedMsg))
return;
}
bool SessionClient::SendProcessInputs(ProcessImpl& process)
{
auto inputs = process.GetTrackedInputs();
u32 left = u32(inputs.size());
u32 capacityToAdd = left;
u8* readPos = inputs.data();
while (left)
{
StackBinaryWriter<SendMaxSize> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_ProcessInputs, writer);
writer.Write7BitEncoded(process.m_id);
writer.Write7BitEncoded(capacityToAdd);
capacityToAdd = 0;
u32 toWrite = Min(left, u32(writer.GetCapacityLeft()));
writer.WriteBytes(readPos, toWrite);
StackBinaryReader<32> reader;
if (!msg.Send(reader))
return false;
readPos += toWrite;
left -= toWrite;
}
return true;
}
bool SessionClient::SendProcessFinished(ProcessImpl& process, u32 exitCode)
{
StackBinaryWriter<SendMaxSize> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_ProcessFinished, writer);
writer.WriteU32(process.m_id);
writer.WriteU32(exitCode);
u32* lineCount = (u32*)writer.AllocWrite(sizeof(u32));
*lineCount = WriteLogLines(writer, process);
// This is normally set after callback so we need to calculate it here
auto& exitTime = process.m_processStats.exitTime;
auto oldExitTime = exitTime.load();
if (exitTime)
exitTime = GetTime() - exitTime;
// Must be written last
process.m_processStats.Write(writer);
process.m_sessionStats.Write(writer);
process.m_storageStats.Write(writer);
process.m_kernelStats.Write(writer);
exitTime = oldExitTime;
StackBinaryReader<16> reader;
if (!msg.Send(reader, m_stats.procFinishedMsg) && m_loop)
return m_logger.Error(TC("Failed to send ProcessFinished message!"));
return true;
}
bool SessionClient::SendUpdateDirectoryTable(StackBinaryReader<SendMaxSize>& reader)
{
TrackWorkScope tws(m_client, AsView(TC("UpdateDir")), ColorWork);
UBA_ASSERT(reader.GetPosition() == 0);
StackBinaryWriter<32> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetDirectoriesFromServer, writer);
writer.WriteU32(m_sessionId);
if (!msg.Send(reader, Stats().getDirsMsg))
return false;
return UpdateDirectoryTableFromServer(reader);
}
bool SessionClient::SendUpdateNameToHashTable(StackBinaryReader<SendMaxSize>& reader)
{
TrackWorkScope tws(m_client, AsView(TC("UpdateHashTable")), ColorWork);
StackBinaryWriter<32> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetNameToHashFromServer, writer);
writer.WriteU32(~u32(0));
SCOPED_WRITE_LOCK(m_nameToHashMemLock, lock);
writer.WriteU32(u32(m_nameToHashTableMem.writtenSize));
if (!msg.Send(reader, Stats().getHashesMsg))
return false;
return UpdateNameToHashTableFromServer(reader);
}
void SessionClient::SendPing(u64 memAvail, u64 memTotal)
{
u64 time = GetTime();
if (TimeToMs(time - m_lastPingSendTime) < 2000) // Ping every ~2 seconds... this is so server can disconnect a client quickly if no ping is coming
return;
float cpuLoad = UpdateCpuLoad();
m_cpuUsage = cpuLoad;
StackBinaryWriter<128> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_Ping, writer);
writer.WriteU32(m_sessionId);
writer.WriteU64(m_lastPing);
writer.WriteU64(memAvail);
writer.WriteU64(memTotal);
writer.WriteU32(*(u32*)&cpuLoad);
StackBinaryReader<32> reader;
struct Response
{
Event done;
u64 time;
} response{true, 0};
auto doneFunc = [](bool error, void* userData)
{
auto& response = *(Response*)userData;
response.time = GetTime();
response.done.Set();
};
time = GetTime();
if (!msg.SendAsync(reader, doneFunc, &response))
{
m_loop = false;
return;
}
bool reportPing = false;
u32 timeoutSeconds = m_pingTimeoutSecondsPrintCallstacks ? m_pingTimeoutSecondsPrintCallstacks : 20;
if (!response.done.IsSet(timeoutSeconds*1000))
{
reportPing = true;
LoggerWithWriter logger(g_consoleLogWriter);
logger.Info(TC("Took more than %u seconds to send/receive ping%s"), timeoutSeconds, (m_pingTimeoutSecondsPrintCallstacks ? TC(". Printing callstacks") : TC("")));
if (m_pingTimeoutSecondsPrintCallstacks)
PrintAllCallstacks(logger);
m_client.ValidateNetwork(logger, true);
while (!response.done.IsSet(2000))
m_client.ValidateNetwork(logger, false);
}
response.done.IsSet(); // Wait forever (until message trigger error or finishes)
if (reportPing)
{
LoggerWithWriter logger(g_consoleLogWriter);
logger.Info(TC("Ping finished after %s"), TimeToText(GetTime() - time).str);
}
if (!msg.ProcessAsyncResults(reader) || msg.GetError())
{
m_loop = false;
return;
}
u64 lastPing = response.time - time;
m_lastPing = lastPing;
m_lastPingSendTime = response.time;
if (lastPing < m_bestPing || m_bestPing == 0)
m_bestPing = lastPing;
m_storage.Ping();
if (reader.ReadBool()) // abort
{
LoggerWithWriter(g_consoleLogWriter).Info(TC("Got abort from server"));
abort();
}
if (reader.ReadBool()) // crashdump
{
TraverseAllCallstacks([&](const CallstackInfo& cs)
{
BinaryReader stackReader(cs.data.data(), 0, cs.data.size());
StackBinaryWriter<SendMaxSize> stackWriter;
GetSymbols(UBA_AGENT_EXECUTABLE, IsArmBinary, stackReader, stackWriter);
BinaryReader resultReader(stackWriter.GetData(), 0, stackWriter.GetPosition());
TString infoString = resultReader.ReadString();
m_logger.Info(TC("%s%s"), cs.desc.c_str(), infoString.c_str());
},
[&](const StringView& error)
{
m_logger.Info(error.data);
});
}
}
void SessionClient::SendNotification(const StringView& text)
{
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_Notification, writer);
writer.WriteU32(m_sessionId);
writer.WriteString(text);
msg.Send();
}
bool SessionClient::SendRootsHandle(RootsHandle rootsHandle)
{
SCOPED_FUTEX(m_rootsLookupLock, rootsLock);
auto insres = m_rootsLookup.try_emplace(WithVfs(rootsHandle, false));
RootsEntry& entry = insres.first->second;
rootsLock.Leave();
SCOPED_FUTEX(entry.lock, entryLock);
if (entry.handled)
return true;
entry.handled = true;
StackBinaryWriter<128> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetRoots, writer);
writer.WriteU64(rootsHandle);
StackBinaryReader<8*1024> reader;
if (!msg.Send(reader, m_stats.getApplicationMsg))
return false;
PopulateRootsEntry(entry, reader.GetPositionData(), reader.GetLeft());
return true;
}
void SessionClient::SendSummary(const Function<void(Logger&)>& extraInfo)
{
StackBinaryWriter<SendMaxSize> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_Summary, writer);
writer.WriteU32(m_sessionId);
WriteSummary(writer, [&](Logger& logger)
{
PrintSummary(logger);
m_storage.PrintSummary(logger);
m_client.PrintSummary(logger);
KernelStats::GetGlobal().Print(logger, true);
PrintContentionSummary(logger);
if (extraInfo)
extraInfo(logger);
});
msg.Send();
}
void SessionClient::SendLogFileToServer(ProcessImpl& pi)
{
auto logFile = pi.m_startInfo.logFile;
if (!logFile || !*logFile)
return;
WrittenFile f;
f.backedName = logFile;
f.attributes = DefaultAttributes();
StringBuffer<> dest;
if (const tchar* lastSlash = TStrrchr(logFile, PathSeparator))
logFile = lastSlash + 1;
dest.Append(TCV("<log>")).Append(logFile);
f.name = dest.data;
f.key = ToStringKeyLower(dest);
SendFile(f, pi.GetId(), false, true);
for (auto& child : pi.m_childProcesses)
SendLogFileToServer(*(ProcessImpl*)child.m_process);
}
void SessionClient::GetLogFileName(StringBufferBase& out, const tchar* logFile, const tchar* arguments, u32 processId)
{
out.Append(m_sessionLogDir.data);
if (logFile && *logFile)
{
if (const tchar* lastSeparator = TStrrchr(logFile, PathSeparator))
logFile = lastSeparator + 1;
out.Append(logFile);
}
else
{
GenerateNameForProcess(out, arguments, processId);
out.Append(TCV(".log"));
}
}
void SessionClient::ThreadCreateProcessLoop()
{
m_sendPing = true;
// Ask for dir and hash table straight away to minimize latency
m_client.AddWork([this](const WorkContext&)
{
StackBinaryReader<SendMaxSize> reader;
SendUpdateDirectoryTable(reader);
}, 1, TC("InitGetDirTable"), ColorWork);
m_client.AddWork([this](const WorkContext&)
{
StackBinaryReader<SendMaxSize> reader;
SendUpdateNameToHashTable(reader);
}, 1, TC("InitGetHashTable"), ColorWork);
struct ProcessRec
{
ProcessRec(ProcessImpl* impl) : handle(impl) {}
ProcessHandle handle;
Futex lock;
Atomic<bool> isKilled;
Atomic<bool> isDone;
float weight = 1.0f;
};
List<ProcessRec> activeProcesses;
u64 lastWaitTime = 0;
u64 waitForMemoryPressureStartTime = 0;
constexpr u64 waitTimeToSpawnAfterKillMs = 5 * 1000;
u64 memAvail = m_memAvail;
u64 memTotal = m_memTotal;
u64 memRequiredToSpawn = u64(double(memTotal) * double(100 - m_memWaitLoadPercent) / 100.0);
u64 memRequiredFree = u64(double(memTotal) * double(100 - m_memKillLoadPercent) / 100.0);
float activeWeight = 0;
ReaderWriterLock activeWeightLock;
u64 idleStartTime = GetTime();
u32 processRequestCount = 0;
auto RemoveInactiveProcesses = [&]()
{
for (auto it=activeProcesses.begin();it!=activeProcesses.end();)
{
ProcessRec& r = *it;
if (!r.isDone)
{
++it;
continue;
}
r.lock.Enter();
r.lock.Leave();
it = activeProcesses.erase(it);
}
if (m_remoteExecutionEnabled && m_terminationReason)
{
m_remoteExecutionEnabled = false;
m_logger.Info(TC("%s. Will stop scheduling processes and send failing processes back for retry"), m_terminationReason.load());
}
if (!activeProcesses.empty() || !m_allowSpawn)
{
idleStartTime = GetTime();
processRequestCount = 0;
}
else if (m_remoteExecutionEnabled)
{
u32 idleTime = u32(TimeToS(GetTime() - idleStartTime));
if (idleTime > m_maxIdleSeconds)
{
m_logger.Info(TC("Session has been idle longer than max idle time (%u seconds). Disconnecting (Did %u process requests during idle)"), m_maxIdleSeconds, processRequestCount);
SendNotification(AsView(TC("Idle time timeout")));
m_waitToSendEvent.Set();
m_remoteExecutionEnabled = false;
}
}
};
Vector<InternalProcessStartInfo> startInfos;
while (m_loop)
{
float maxWeight = float(m_maxProcessCount);
u32 waitTimeoutMs = 3000;
FlushDeadProcesses();
GetMemoryInfo(memAvail, memTotal);
m_memAvail = memAvail;
m_memTotal = memTotal;
if (memAvail < memRequiredFree)
{
for (auto it = activeProcesses.rbegin(); it != activeProcesses.rend(); ++it)
{
ProcessRec& rec = *it;
if (rec.isKilled || rec.isDone)
continue;
SCOPED_FUTEX(rec.lock, lock);
if (rec.isDone)
continue;
rec.handle.Cancel(true);
rec.isKilled = true;
SendReturnProcess(rec.handle.GetId(), TC("Running out of memory"));
++m_stats.killCount;
m_logger.Warning(TC("Killed process due to memory pressure (Available: %s Total: %s)"), BytesToText(memAvail).str, BytesToText(memTotal).str);
break;
}
lastWaitTime = GetTime();
}
bool canSpawn = TimeToMs(GetTime() - lastWaitTime) > waitTimeToSpawnAfterKillMs && m_allowSpawn;
if (!canSpawn)
waitTimeoutMs = 500;
bool firstCall = true;
while (m_remoteExecutionEnabled && canSpawn && m_loop)
{
float availableWeight;
{
SCOPED_READ_LOCK(activeWeightLock, lock);
if (activeWeight >= maxWeight)
break;
availableWeight = maxWeight - activeWeight;
}
if (!firstCall)
{
GetMemoryInfo(memAvail, memTotal);
m_memAvail = memAvail;
m_memTotal = memTotal;
}
if (memAvail < memRequiredToSpawn)
{
if (waitForMemoryPressureStartTime == 0)
{
m_logger.Info(TC("Delaying spawn due to memory pressure (Available: %s Total: %s)"), BytesToText(memAvail).str, BytesToText(memTotal).str);
waitForMemoryPressureStartTime = GetTime();
}
break;
}
if (waitForMemoryPressureStartTime)
{
u64 waitTime = GetTime() - waitForMemoryPressureStartTime;
m_logger.Info(TC("Waited %s for memory pressure to go down (Available: %s Total: %s)"), TimeToText(waitTime).str, BytesToText(memAvail).str, BytesToText(memTotal).str);
m_stats.waitMemPressure += waitTime;
waitForMemoryPressureStartTime = 0;
lastWaitTime = GetTime();
waitTimeoutMs = 200;
availableWeight = Min(availableWeight, 1.0f);
}
startInfos.clear();
if (!SendProcessAvailable(startInfos, availableWeight))
{
m_loop = false;
break;
}
++processRequestCount;
if (!m_remoteExecutionEnabled)
{
m_logger.Info(TC("Got remote execution disabled response from host (will finish %llu active processes)"), startInfos.size() + activeProcesses.size());
}
if (startInfos.empty())
{
canSpawn = false;
waitTimeoutMs = 200;
}
for (InternalProcessStartInfo& startInfo : startInfos)
{
startInfo.uiLanguage = int(m_uiLanguage);
startInfo.priorityClass = m_defaultPriorityClass;
startInfo.useCustomAllocator = !m_disableCustomAllocator;
startInfo.rules = GetRules(startInfo);
StringBuffer<> logFile;
if (m_logToFile || (*startInfo.logFile && m_shouldSendLogToServer))
{
GetLogFileName(logFile, startInfo.logFile, startInfo.arguments, startInfo.processId);
startInfo.logFile = logFile.data;
}
void* env = GetProcessEnvironmentVariables();
auto process = new ProcessImpl(*this, startInfo.processId, nullptr, true);
activeProcesses.emplace_back(process);
ProcessRec* rec = &activeProcesses.back();
rec->weight = startInfo.weight;
{
SCOPED_WRITE_LOCK(activeWeightLock, lock);
activeWeight += rec->weight;
}
struct ExitedRec
{
ExitedRec(SessionClient& s, ReaderWriterLock& l, float& w, ProcessRec* r) : session(s), activeWeightLock(l), activeWeight(w), rec(r) {}
SessionClient& session;
ReaderWriterLock& activeWeightLock;
float& activeWeight;
ProcessRec* rec;
};
ExitedRec* exitedRec = new ExitedRec(*this, activeWeightLock, activeWeight, rec);
startInfo.userData = exitedRec;
startInfo.exitedFunc = [](void* userData, const ProcessHandle& h, ProcessExitedResponse& exitedResponse)
{
auto er = (ExitedRec*)userData;
SessionClient& session = er->session;
ReaderWriterLock& activeWeightLock = er->activeWeightLock;
float& activeWeight = er->activeWeight;
ProcessRec* rec = er->rec;
delete er;
auto& startInfo = h.GetStartInfo();
if (session.m_shouldSendLogToServer)
session.SendLogFileToServer(*(ProcessImpl*)h.m_process);
float weight = rec->weight;
auto decreaseWeight = MakeGuard([&]()
{
SCOPED_WRITE_LOCK(activeWeightLock, weightLock);
activeWeight -= weight;
session.m_waitToSendEvent.Set();
});
SCOPED_FUTEX(rec->lock, lock);
auto doneGuard = MakeGuard([&]() { rec->isDone = true; session.m_waitToSendEvent.Set(); });
if (rec->isKilled)
return;
auto& process = *(ProcessImpl*)h.m_process;
if (session.m_killRandomIndex != ~0u && session.m_killRandomCounter++ == session.m_killRandomIndex)
{
session.m_loop = false;
session.m_logger.Info(TC("Killed random process (%s)"), process.m_startInfo.GetDescription());
return;
}
u32 exitCode = process.m_exitCode;
if (exitCode != 0)
{
if (GetTime() >= session.m_terminationTime)
{
if (session.m_loop)
session.SendReturnProcess(rec->handle.GetId(), session.m_terminationReason);
return;
}
if (process.HasFailedMessage()) // If there are failure caused by failed messages we send back for retry
{
if (session.m_loop)
session.SendReturnProcess(rec->handle.GetId(), TC("Failed message"));
return;
}
}
if (exitCode == 0 || startInfo.writeOutputFilesOnFail)
{
// Should we decrease weight before or after sending files?
//decreaseWeight.Execute();
if (!session.SendFiles(process, process.m_processStats.sendFiles))
{
const tchar* desc = TC("Failed to send output files to host");
session.m_logger.Error(desc);
if (session.m_loop)
session.SendReturnProcess(rec->handle.GetId(), desc);
return;
}
}
decreaseWeight.Execute();
if (process.IsCancelled())
{
if (session.m_loop)
session.SendReturnProcess(rec->handle.GetId(), TC("Cancelled"));
return;
}
if (startInfo.trackInputs)
session.SendProcessInputs(process);
session.SendProcessFinished(process, exitCode);
// TODO: These should be removed and instead added in TraceReader (so it will update over time)
session.m_stats.stats.Add(process.m_sessionStats);
session.m_storage.AddStats(process.m_storageStats);
if (session.m_processFinished)
session.m_processFinished(&process);
};
if (!process->Start(startInfo, true, env, true)) // If false, exitFunction is not called
{
SendReturnProcess(rec->handle.GetId(), TC("Failed to find executable"));
activeProcesses.pop_back();
delete exitedRec;
m_remoteExecutionEnabled = false;
}
}
RemoveInactiveProcesses();
firstCall = false;
}
//SendUpdateNameToHashTable(); // It is always nice to populate this at certain cadence since it might speed up running processes queries
m_waitToSendEvent.IsSet(waitTimeoutMs);
RemoveInactiveProcesses();
if (activeProcesses.empty() && !m_remoteExecutionEnabled)
{
// There can be processes that are done (isDone is true) but are still in m_processes list (since they are removed from that after). give them some time
u64 counter = 300;
while (true)
{
if (!counter--)
{
m_logger.Warning(TC("Took a long time for processes to be removed after being finished"));
break;
}
SCOPED_FUTEX_READ(m_processesLock, processesLock);
if (m_processes.empty())
break;
processesLock.Leave();
Sleep(10);
}
break;
}
}
CancelAllProcessesAndWait(); // If we got the exit from server there is no point sending anything more back.. cancel everything
u32 retry = 0;
while (true)
{
if (retry++ == 100)
{
m_logger.Error(TC("This should never happen!"));
break;
}
RemoveInactiveProcesses();
if (activeProcesses.empty())
break;
m_waitToSendEvent.IsSet(100);
};
m_client.FlushWork();
StopTraceThread();
if (m_shouldSendTraceToServer)
{
// Can't disable this, it can cause race conditions
//m_client.SetWorkTracker(nullptr);
StackBinaryWriter<SendMaxSize> writer;
WriteSummary(writer, [&](Logger& logger)
{
PrintSummary(logger);
m_storage.PrintSummary(logger);
m_client.PrintSummary(logger);
KernelStats::GetGlobal().Print(logger, true);
PrintContentionSummary(logger);
});
m_trace.SessionSummary(0, writer.GetData(), writer.GetPosition());
StringBuffer<> ubaFile(m_sessionLogDir);
ubaFile.Append(TCV("Trace.uba"));
if (StopTrace(ubaFile.data))
{
WrittenFile f;
f.backedName = ubaFile.data;
f.attributes = DefaultAttributes();
StringBuffer<> dest(TC("<uba>"));
f.name = dest.data;
f.key = ToStringKeyLower(dest);
SendFile(f, 0, false, true);
}
}
}
u32 SessionClient::WriteLogLines(BinaryWriter& writer, ProcessImpl& process)
{
u32 logLineCount = 0;
for (auto& child : process.m_childProcesses)
logLineCount += WriteLogLines(writer, *(ProcessImpl*)child.m_process);
for (auto& line : process.m_logLines)
{
if (line.text.size()*sizeof(tchar) + 1000 >= writer.GetCapacityLeft())
break;
writer.WriteString(line.text);
writer.WriteByte(line.type);
++logLineCount;
}
return logLineCount;
}
bool SessionClient::ParseDirectoryTable()
{
SCOPED_WRITE_LOCK(m_directoryTable.m_lookupLock, lock);
SCOPED_READ_LOCK(m_directoryTable.m_memoryLock, lock2);
u32 newMemPosition = m_directoryTable.m_memorySize;
lock2.Leave();
if (newMemPosition == m_dirtableParsedPosition)
return false;
m_directoryTable.ParseDirectoryTableNoLock(m_dirtableParsedPosition, newMemPosition);
m_dirtableParsedPosition = newMemPosition;
return true;
}
bool SessionClient::EntryExists(const StringView& path, u32& outTableOffset)
{
StringKey key;
if (path.data[path.count - 1] == PathSeparator)
key = ToStringKey(path.data, path.count - 1);
else
key = ToStringKey(path);
StringBuffer<> dirName;
u32 tableOffset = 0;
DirectoryTable::Exists exists = m_directoryTable.EntryExists(key, path, true, &tableOffset);
if (exists == DirectoryTable::Exists_Maybe)
{
if (ParseDirectoryTable())
{
exists = m_directoryTable.EntryExists(key, path, true, &tableOffset);
}
if (exists == DirectoryTable::Exists_Maybe)
{
if (const tchar* lastSeparator = TStrrchr(path.data, PathSeparator))
dirName.Append(path.data, lastSeparator - path.data);
StringKey dirKey = ToStringKey(dirName);
{ // Optimization to reduce number of messages
SCOPED_FUTEX(m_dirVisitedLock, l);
DirVisitedEntry& entry = m_dirVisited[dirKey];
l.Leave();
SCOPED_FUTEX(entry.lock, l2);
if (!entry.handled)
{
ListDirectoryResponse out;
if (!GetListDirectoryInfo(out, dirName, dirKey))
return false;
ParseDirectoryTable();
entry.handled = true;
}
}
exists = m_directoryTable.EntryExists(key, path, true, &tableOffset);
}
}
UBA_ASSERTF(exists != DirectoryTable::Exists_Maybe, TC("This should not happen. Asking for directory %s"), dirName.data);
if (exists != DirectoryTable::Exists_Yes)
return false;
outTableOffset = tableOffset;
return true;
}
bool SessionClient::AllocFailed(Process& process, const tchar* allocType, u32 error)
{
//StackBinaryWriter<32> writer;
//NetworkMessage msg(m_client, ServiceId, SessionMessageType_VirtualAllocFailed, writer);
//if (!msg.Send())
// m_logger.Error(TC("Failed to send VirtualAllocFailed message!"));
return Session::AllocFailed(process, allocType, error);
}
void SessionClient::PrintSessionStats(Logger& logger)
{
Session::PrintSessionStats(logger);
}
bool SessionClient::GetNextProcess(Process& process, bool& outNewProcess, NextProcessInfo& outNextProcess, u32 prevExitCode, BinaryReader& statsReader)
{
outNewProcess = false;
if (!m_remoteExecutionEnabled)
return true;
auto& pi = (ProcessImpl&)process;
if (!FlushWrittenFiles(pi))
return false;
ProcessStats processStats;
processStats.Read(statsReader, TraceVersion);
processStats.sendFiles = pi.m_processStats.sendFiles;
StackBinaryReader<SendMaxSize> reader;
StackBinaryWriter<16 * 1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetNextProcess, writer);
writer.WriteU32(pi.m_id);
writer.WriteU32(prevExitCode);
processStats.Write(writer);
writer.WriteBytes(statsReader.GetPositionData(), statsReader.GetLeft());
if (!msg.Send(reader, m_stats.customMsg))
return false;
outNewProcess = reader.ReadBool();
if (outNewProcess)
{
if (m_shouldSendLogToServer)
SendLogFileToServer(pi);
pi.m_exitCode = prevExitCode;
if (m_processFinished)
m_processFinished(&process);
outNextProcess.arguments = reader.ReadString();
outNextProcess.workingDir = reader.ReadString();
outNextProcess.description = reader.ReadString();
outNextProcess.logFile = reader.ReadString();
if (m_logToFile || (!outNextProcess.logFile.empty() && m_shouldSendLogToServer))
{
StringBuffer<512> logFile;
GetLogFileName(logFile, outNextProcess.logFile.c_str(), outNextProcess.arguments.c_str(), process.GetId());
outNextProcess.logFile = logFile.data;
}
}
return SendUpdateDirectoryTable(reader.Reset());
}
bool SessionClient::CustomMessage(Process& process, BinaryReader& reader, BinaryWriter& writer)
{
StackBinaryWriter<SendMaxSize> msgWriter;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_Custom, msgWriter);
u32 recvSize = reader.ReadU32();
msgWriter.WriteU32(process.GetId());
msgWriter.WriteU32(recvSize);
msgWriter.WriteBytes(reader.GetPositionData(), recvSize);
BinaryReader msgReader(writer.GetData(), 0);
if (!msg.Send(msgReader, m_stats.customMsg))
return false;
u32 responseSize = msgReader.ReadU32();
writer.AllocWrite(4ull + responseSize);
return true;
}
bool SessionClient::SHGetKnownFolderPath(Process& process, BinaryReader& reader, BinaryWriter& writer)
{
#if PLATFORM_WINDOWS
StackBinaryWriter<SendMaxSize> msgWriter;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_SHGetKnownFolderPath, msgWriter);
msgWriter.WriteBytes(reader.GetPositionData(), reader.GetLeft());
BinaryReader msgReader(writer.GetData(), 0);
if (!msg.Send(msgReader, m_stats.customMsg))
{
writer.WriteU32(u32(E_FAIL));
return false;
}
writer.AllocWrite(msgReader.GetPosition());
#endif
return true;
}
bool SessionClient::HostRun(BinaryReader& reader, BinaryWriter& writer)
{
const void* data = reader.GetPositionData();
u64 size = reader.GetLeft();
CasKey key = ToCasKey(CasKeyHasher().Update(data, size), false);
SCOPED_FUTEX(m_hostRunCacheLock, l);
auto insres = m_hostRunCache.try_emplace(key);
auto& buffer = insres.first->second;
if (!insres.second)
{
writer.WriteBytes(buffer.data(), buffer.size());
return true;
}
StackBinaryWriter<SendMaxSize> msgWriter;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_HostRun, msgWriter);
msgWriter.WriteBytes(data, size);
BinaryReader msgReader(writer.GetData(), 0);
if (!msg.Send(msgReader, m_stats.customMsg))
return false;
writer.AllocWrite(msgReader.GetLeft());
buffer.resize(msgReader.GetLeft());
memcpy(buffer.data(), msgReader.GetPositionData(), buffer.size());
return true;
}
bool SessionClient::GetSymbols(const tchar* application, bool isArm, BinaryReader& reader, BinaryWriter& writer)
{
StackBinaryWriter<SendMaxSize> msgWriter;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_GetSymbols, msgWriter);
msgWriter.WriteString(application);
msgWriter.WriteBool(isArm);
u32 size = reader.ReadU32();
msgWriter.WriteU32(size);
msgWriter.WriteBytes(reader.GetPositionData(), size);
BinaryReader responseReader(writer.GetData(), 0, writer.GetCapacityLeft());
if (!msg.Send(responseReader, m_stats.customMsg))
return false;
writer.AllocWrite(responseReader.GetLeft());
if constexpr (DownloadDebugSymbols)
{
CasKey detoursSymbolsKey = responseReader.ReadCasKey();
if (detoursSymbolsKey == CasKeyZero)
return true;
StringBuffer<128> symbolsFile(UBA_DETOURS_LIBRARY);
#if PLATFORM_WINDOWS
symbolsFile.Resize(symbolsFile.count - 3).Append("pdb");
#else
symbolsFile.Resize(symbolsFile.count - 2).Append("debug");
#endif
Storage::RetrieveResult result;
StringBuffer<> throwaway;
if (m_storage.RetrieveCasFile(result, AsCompressed(detoursSymbolsKey, false), symbolsFile.data))
WriteBinFile(throwaway, symbolsFile, detoursSymbolsKey, KeyToString(StringKeyZero), DefaultAttributes());
}
return true;
}
bool SessionClient::FlushWrittenFiles(ProcessImpl& process)
{
SCOPED_FUTEX(process.m_shared.writtenFilesLock, lock);
bool success = SendFiles(process, process.m_processStats.sendFiles);
{
SCOPED_FUTEX(m_outputFilesLock, lock2);
for (auto& kv : process.m_shared.writtenFiles)
m_outputFiles.erase(kv.second.name);
}
process.m_shared.writtenFiles.clear();
return success;
}
bool SessionClient::UpdateEnvironment(ProcessImpl& process, const StringView& reason, bool resetStats)
{
StackBinaryReader<SendMaxSize> reader;
if (resetStats)
{
StackBinaryWriter<16 * 1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_UpdateEnvironment, writer);
writer.WriteU32(process.m_id);
writer.WriteString(reason);
process.m_processStats.Write(writer);
process.m_sessionStats.Write(writer);
process.m_storageStats.Write(writer);
process.m_kernelStats.Write(writer);
process.m_processStats = {};
process.m_sessionStats = {};
process.m_storageStats = {};
process.m_kernelStats = {};
if (!msg.Send(reader, m_stats.customMsg))
return false;
reader.Reset();
}
return SendUpdateDirectoryTable(reader);
}
bool SessionClient::LogLine(ProcessImpl& process, const tchar* line, LogEntryType logType)
{
// Remove this once we have figured out a bug that seems to exist for remote execution
// Update: Bug has been found for macos... for windows we believe the bug is related to uninformed shutdown and having multiple tcp connections..
// ... one tcp connection is disconnected, causing file not found while another connection manages to send "process finished"
#if 0 // PLATFORM_WINDOWS
auto rules = process.m_startInfo.rules;
if (!rules)
return true;
const tchar* errorPos = nullptr;
if (rules->index == SpecialRulesIndex_ClExe)
{
if (!Contains(line, TC("C1083"), false, &errorPos))
return true;
}
else
{
if (!Contains(line, TC("' file not found")))
return true;
if (!Contains(line, TC("fatal error: '"), false, &errorPos))
return true;
}
const tchar* fileBegin = TStrchr(errorPos, '\'');
if (!fileBegin)
return true;
++fileBegin;
const tchar* fileEnd = TStrchr(fileBegin, '\'');
if (!fileEnd)
return true;
MemoryBlock memoryBlock;
DirectoryTable dirTable(&memoryBlock);
{
SCOPED_WRITE_LOCK(m_directoryTable.m_memoryLock, lock2);
m_directoryTable.m_memorySize = m_directoryTableMemPos;
dirTable.Init(m_directoryTable.m_memory, 0, m_directoryTable.m_memorySize);
}
StringBuffer<> errorPath;
errorPath.Append(fileBegin, fileEnd - fileBegin).Replace('/', PathSeparator);
{
StackBinaryWriter<1024> writer;
NetworkMessage msg(m_client, ServiceId, SessionMessageType_DebugFileNotFoundError, writer);
writer.WriteString(errorPath);
writer.WriteString(process.m_startInfo.workingDir);
msg.Send();
}
StringView searchString = errorPath;
if (searchString.data[0] == '.' && searchString.data[1] == '.')
{
searchString.data += 3;
searchString.count -= 3;
}
u32 foundCount = 0;
dirTable.TraverseAllFilesNoLock([&](const DirectoryTable::EntryInformation& info, const StringBufferBase& path, u32 dirOffset)
{
if (!path.EndsWith(searchString))
return;
if (path[path.count - searchString.count - 1] != PathSeparator)
return;
auto ToString = [](bool b) { return b ? TC("true") : TC("false"); };
++foundCount;
StringBuffer<> logStr;
logStr.Appendf(TC("File %s found in directory table at offset %u of %u while searching for matches for %s (File size %llu attr %u)"), path.data, dirOffset, dirTable.m_memorySize, searchString.data, info.size, info.attributes);
process.LogLine(false, logStr.data, logType);
StringKey fileNameKey = ToStringKey(path);
SCOPED_READ_LOCK(m_fileMappingTableLookupLock, mlock);
auto findIt = m_fileMappingTableLookup.find(fileNameKey);
if (findIt != m_fileMappingTableLookup.end())
{
auto& entry = findIt->second;
SCOPED_READ_LOCK(entry.lock, entryCs);
logStr.Clear().Appendf(TC("File %s found in mapping table table."), path.data);
if (entry.handled)
{
StringBuffer<128> mappingName;
if (entry.mapping.IsValid())
Storage::GetMappingString(mappingName, entry.mapping, entry.mappingOffset);
else
mappingName.Append(TCV("Not valid"));
logStr.Appendf(TC(" Success: %s Size: %u IsDir: %s Mapping name: %s Mapping offset: %u"), ToString(entry.success), entry.size, ToString(entry.isDir), mappingName.data, entry.mappingOffset);
}
else
{
logStr.Appendf(TC(" Entry not handled"));
}
}
else
logStr.Clear().Appendf(TC("File %s not found in mapping table table."), path.data);
process.LogLine(false, logStr.data, logType);
CasKey key;
if (GetCasKeyForFile(key, process.m_id, path, fileNameKey))
{
logStr.Clear().Appendf(TC("File %s caskey is %s."), path.data, CasKeyString(key).str);
StringBuffer<512> casKeyFile;
if (m_storage.GetCasFileName(casKeyFile, key))
{
logStr.Appendf(TC(" CasKeyFile: %s"), casKeyFile.data);
u64 size = 0;
u32 attributes = 0;
bool exists = FileExists(m_logger, casKeyFile.data, &size, &attributes);
logStr.Appendf(TC(" Exists: %s"), ToString(exists));
if (exists)
{
logStr.Appendf(TC(" Size: %llu Attr: %u"), size, attributes);
FileHandle fileHandle = uba::CreateFileW(casKeyFile.data, GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING, DefaultAttributes());
if (fileHandle == InvalidFileHandle)
{
logStr.Appendf(TC(" Failed to open file %s (%s)"), casKeyFile.data, LastErrorToText().data);
}
else
{
logStr.Appendf(TC(" CreateFile for read successful"));
uba::CloseFile(casKeyFile.data, fileHandle);
}
}
}
else
logStr.Appendf(TC(" Failed to get cas filename for cas key"));
}
else
logStr.Clear().Appendf(TC("File %s caskey not found"), path.data);
process.LogLine(false, logStr.data, logType);
});
if (!foundCount)
{
StringBuffer<> logStr;
logStr.Appendf(TC("No matching entry found in directory table while searching for matches for %s. DirTable size: %u"), searchString.data, GetDirectoryTableSize());
process.LogLine(false, logStr.data, logType);
if (errorPath.StartsWith(TC("..\\Intermediate")))
{
auto workDir = process.m_startInfo.workingDir;
StringBuffer<> fullPath;
FixPath(errorPath.data, workDir, TStrlen(workDir), fullPath);
}
}
#endif
return true;
}
void SessionClient::TraceSessionUpdate()
{
if (m_loop && m_sendPing)
SendPing(m_memAvail, m_memTotal);
// TODO: There should be some sort of log entry if we have a process that has done no progress for 30 minutes
if (!m_trace.IsWriting())
return;
u64 send;
u64 recv;
if (auto backend = m_client.GetFirstConnectionBackend())
{
backend->GetTotalSendAndRecv(send, recv);
}
else
{
recv = m_client.GetTotalRecvBytes();
send = m_client.GetTotalSentBytes();
}
// send and recv are swapped on purpose because that is how visualizer is visualizing
m_trace.SessionUpdate(0, 0, send, recv, m_lastPing, m_memAvail, m_memTotal, m_cpuUsage);
}
}