// 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 #include #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 modules; if (!ReadModules(modules, 0, application)) return false; StringBuffer applicationDir; applicationDir.AppendDir(application); KeyToString keyStr(ToStringKeyLower(applicationDir)); Atomic 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 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& 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 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 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 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 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 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 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& 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& 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 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& out, float availableWeight) { StackBinaryReader 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 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 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& 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& 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 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& extraInfo) { StackBinaryWriter 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("")).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 reader; SendUpdateDirectoryTable(reader); }, 1, TC("InitGetDirTable"), ColorWork); m_client.AddWork([this](const WorkContext&) { StackBinaryReader reader; SendUpdateNameToHashTable(reader); }, 1, TC("InitGetHashTable"), ColorWork); struct ProcessRec { ProcessRec(ProcessImpl* impl) : handle(impl) {} ProcessHandle handle; Futex lock; Atomic isKilled; Atomic isDone; float weight = 1.0f; }; List 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 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 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("")); 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 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 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 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 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 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 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); } }