// Copyright Epic Games, Inc. All Rights Reserved. #include "Installer/Verifier.h" #include "HAL/ThreadSafeBool.h" #include "Common/StatsCollector.h" #include "Common/FileSystem.h" #include "BuildPatchVerify.h" #include "BuildPatchUtil.h" #include "HAL/UESemaphore.h" #include "IBuildManifestSet.h" #include "Misc/ConfigCacheIni.h" DEFINE_LOG_CATEGORY_STATIC(LogVerifier, Log, All); namespace BuildPatchServices { bool TryConvertToVerifyResult(EVerifyError InVerifyError, EVerifyResult& OutVerifyResult) { switch (InVerifyError) { case EVerifyError::FileMissing: OutVerifyResult = EVerifyResult::FileMissing; return true; case EVerifyError::OpenFileFailed: OutVerifyResult = EVerifyResult::OpenFileFailed; return true; case EVerifyError::HashCheckFailed: OutVerifyResult = EVerifyResult::HashCheckFailed; return true; case EVerifyError::FileSizeFailed: OutVerifyResult = EVerifyResult::FileSizeFailed; return true; } return false; } bool TryConvertToVerifyError(EVerifyResult InVerifyResult, EVerifyError& OutVerifyError) { switch (InVerifyResult) { case EVerifyResult::FileMissing: OutVerifyError = EVerifyError::FileMissing; return true; case EVerifyResult::OpenFileFailed: OutVerifyError = EVerifyError::OpenFileFailed; return true; case EVerifyResult::HashCheckFailed: OutVerifyError = EVerifyError::HashCheckFailed; return true; case EVerifyResult::FileSizeFailed: OutVerifyError = EVerifyError::FileSizeFailed; return true; } return false; } class FVerifier : public IVerifier { public: FVerifier(IFileSystem* FileSystem, IVerifierStat* InVerificationStat, EVerifyMode InVerifyMode, IBuildInstallerSharedContextPtr InSharedContext, IBuildManifestSet* InManifestSet, FString InVerifyDirectory, FString InStagedFileDirectory); ~FVerifier() {} // IControllable interface begin. virtual void SetPaused(bool bInIsPaused) override; virtual void Abort() override; // IControllable interface end. // IVerifier interface begin. virtual EVerifyResult Verify(TArray& CorruptFiles) override; virtual void AddTouchedFiles(const TSet& TouchedFiles) override; // IVerifier interface end. private: FString SelectFullFilePath(const FString& BuildFile); EVerifyResult VerfiyFileSha(TArray& ReadBuffer, const FString& BuildFile, const FFileManifest& BuildFileManifest); EVerifyResult VerfiyFileSize(const FString& BuildFile, const FFileManifest& BuildFileManifest); private: const FString VerifyDirectory; const FString StagedFileDirectory; IFileSystem* const FileSystem; IVerifierStat* const VerifierStat; IBuildManifestSet* const ManifestSet; IBuildInstallerSharedContextPtr SharedContext; EVerifyMode VerifyMode; TSet FilesToVerify; TSet FilesPassedVerify; FThreadSafeBool bIsPaused; FThreadSafeBool bShouldAbort; struct FThreadVerifyJob { const FFileManifest* BuildFileManifest; bool bVerifySha; FString FileName; }; FEvent* AllThreadsDoneEvent; std::atomic_int64_t ThreadProcessedBytes; TArray ThreadJobResults; UE::FMutex ThreadJobListLock; int32 NextThreadJobListIndex = 0; TArray ThreadJobList; TArray SecondaryProcessThreads; void ProcessVerifyJobs(FSemaphore* ThreadDoneSem); }; FVerifier::FVerifier(IFileSystem* InFileSystem, IVerifierStat* InVerificationStat, EVerifyMode InVerifyMode, IBuildInstallerSharedContextPtr InSharedContext, IBuildManifestSet* InManifestSet, FString InVerifyDirectory, FString InStagedFileDirectory) : VerifyDirectory(MoveTemp(InVerifyDirectory)) , StagedFileDirectory(MoveTemp(InStagedFileDirectory)) , FileSystem(InFileSystem) , VerifierStat(InVerificationStat) , ManifestSet(InManifestSet) , SharedContext(InSharedContext) , VerifyMode(InVerifyMode) , bIsPaused(false) , bShouldAbort(false) { ManifestSet->GetFilesTaggedForRepair(FilesToVerify); } void FVerifier::ProcessVerifyJobs(FSemaphore* ThreadDoneSem) { TArray FileReadBuffer; for (;;) { if (bShouldAbort) { break; } // Get a thread index. int32 OurJobIndex = -1; { UE::TUniqueLock _(ThreadJobListLock); if (NextThreadJobListIndex < ThreadJobList.Num()) { OurJobIndex = NextThreadJobListIndex; NextThreadJobListIndex++; } } if (OurJobIndex == -1) { // No work to do. break; } // Only allocate our buffer when we get a job FileReadBuffer.SetNumUninitialized(4 << 20); // 4MB chunked reads. FThreadVerifyJob& OurJob = ThreadJobList[OurJobIndex]; VerifierStat->OnFileStarted(OurJob.FileName, OurJob.BuildFileManifest->FileSize); EVerifyResult FileVerifyResult = OurJob.bVerifySha ? VerfiyFileSha(FileReadBuffer, OurJob.FileName, *OurJob.BuildFileManifest) : VerfiyFileSize(OurJob.FileName, *OurJob.BuildFileManifest); VerifierStat->OnFileCompleted(OurJob.FileName, FileVerifyResult); ThreadJobResults[OurJobIndex] = FileVerifyResult; } if (ThreadDoneSem) { ThreadDoneSem->Release(1); } // We can't touch _anything_ after here because the outer class holding the lambda could // be destroyed. } void FVerifier::SetPaused(bool bInIsPaused) { bIsPaused = bInIsPaused; } void FVerifier::Abort() { bShouldAbort = true; } EVerifyResult FVerifier::Verify(TArray& CorruptFiles) { TRACE_CPUPROFILER_EVENT_SCOPE(Verify); bShouldAbort = false; EVerifyResult VerifyResult = EVerifyResult::Success; CorruptFiles.Empty(); // If we check all files, grab them all now. if (VerifyMode == EVerifyMode::FileSizeCheckAllFiles || VerifyMode == EVerifyMode::ShaVerifyAllFiles) { ManifestSet->GetExpectedFiles(FilesToVerify); } // Setup progress tracking. TSet VerifyList = FilesToVerify.Difference(FilesPassedVerify); VerifierStat->OnProcessedDataUpdated(0); VerifierStat->OnTotalRequiredUpdated(ManifestSet->GetTotalNewFileSize(VerifyList)); // Select verify function. const bool bVerifyShaMode = VerifyMode == EVerifyMode::ShaVerifyAllFiles || VerifyMode == EVerifyMode::ShaVerifyTouchedFiles; ThreadJobList.Reset(); ThreadJobResults.Reset(); ThreadProcessedBytes = 0; NextThreadJobListIndex = 0; for (const FString& BuildFile : VerifyList) { FThreadVerifyJob Job; Job.FileName = BuildFile; Job.BuildFileManifest = ManifestSet->GetNewFileManifest(BuildFile); Job.bVerifySha = bVerifyShaMode || ManifestSet->IsFileRepairAction(BuildFile); ThreadJobList.Add(MoveTemp(Job)); ThreadJobResults.Add(EVerifyResult::Aborted); } AllThreadsDoneEvent = FPlatformProcess::GetSynchEventFromPool(); // Events get reset when they are returned to the pool but if there are any outstanding // triggers due to races in the previous user we would have a triggered event, so we Reset here. // This doesn't really remove the race but it prevents it from sitting latent for long periods of time. AllThreadsDoneEvent->Reset(); if (SharedContext) { // 3 cores should saturate any modern drive, and on anything that isn't we just sit in the wait // state anyway. int32 ThreadsToCreate = 1; GConfig->GetInt(TEXT("BuildPatchTool"), TEXT("VerificationThreadCount"), ThreadsToCreate, GEngineIni); int32 ThreadsRemaining = ThreadsToCreate; FSemaphore ThreadDoneSem(0, ThreadsToCreate); // We wait to create our threads so that the shared threads from file construction are returned. for (int i = 0; i < ThreadsToCreate; i++) { SecondaryProcessThreads.Add(SharedContext->CreateThread()); SecondaryProcessThreads[i]->RunTask([this, &ThreadDoneSem]() { ProcessVerifyJobs(&ThreadDoneSem); }); } for (;;) { if (ThreadDoneSem.TryAcquire(100)) { ThreadsRemaining--; if (ThreadsRemaining == 0) { // We acquired all the counts so we're done. break; } } // Update our overall progress tracker. VerifierStat->OnProcessedDataUpdated(ThreadProcessedBytes.load(std::memory_order_acquire)); } // If our event is triggered then something has gone very wrong since we could not have triggered it. // This is still here to try and figure out what's been going wrong with it before we switched to a semaphore. bool bEventIsTriggered = AllThreadsDoneEvent->Wait(0); if (!ensure(!bEventIsTriggered)) { UE_LOG(LogVerifier, Error, TEXT("Verifier has a triggered event!")); } for (int i = 0; i < ThreadsToCreate; i++) { SharedContext->ReleaseThread(SecondaryProcessThreads[i]); } SecondaryProcessThreads.Empty(); } else { // Can't create threads, just do the work here. ProcessVerifyJobs(nullptr); } FPlatformProcess::ReturnSynchEventToPool(AllThreadsDoneEvent); AllThreadsDoneEvent = nullptr; // move results over. for (int32 i=0; i& TouchedFiles) { FilesToVerify.Append(TouchedFiles); FilesPassedVerify = FilesPassedVerify.Difference(TouchedFiles); } FString FVerifier::SelectFullFilePath(const FString& BuildFile) { FString FullFilePath; if (StagedFileDirectory.IsEmpty() == false) { FullFilePath = StagedFileDirectory / BuildFile; int64 FileSize; if (FileSystem->GetFileSize(*FullFilePath, FileSize)) { return FullFilePath; } } FullFilePath = VerifyDirectory / BuildFile; return FullFilePath; } EVerifyResult FVerifier::VerfiyFileSha(TArray& ReadBuffer, const FString& BuildFile, const FFileManifest& BuildFileManifest) { TRACE_CPUPROFILER_EVENT_SCOPE(VerifyFileSha); ISpeedRecorder::FRecord ActivityRecord; EVerifyResult VerifyResult; const FString FileToVerify = SelectFullFilePath(BuildFile); uint8 ReturnValue = 0; TUniquePtr FileReader = FileSystem->CreateFileReader(*FileToVerify); VerifierStat->OnFileProgress(BuildFile, 0); if (FileReader.IsValid()) { const int64 FileSize = FileReader->TotalSize(); if (FileSize != BuildFileManifest.FileSize) { VerifyResult = EVerifyResult::FileSizeFailed; } else { FSHA1 HashState; FSHAHash HashValue; while (!FileReader->AtEnd() && !bShouldAbort) { // Pause if necessary while (bIsPaused && !bShouldAbort) { FPlatformProcess::Sleep(0.1f); } ActivityRecord.CyclesStart = FStatsCollector::GetCycles(); // Read file and update hash state const int64 SizeLeft = FileSize - FileReader->Tell(); ActivityRecord.Size = FMath::Min(ReadBuffer.Num(), SizeLeft); { TRACE_CPUPROFILER_EVENT_SCOPE(VerifyFileSha_Serialize); FileReader->Serialize(ReadBuffer.GetData(), ActivityRecord.Size); } { TRACE_CPUPROFILER_EVENT_SCOPE(VerifyFileSha_Hash); HashState.Update(ReadBuffer.GetData(), ActivityRecord.Size); } ActivityRecord.CyclesEnd = FStatsCollector::GetCycles(); VerifierStat->OnFileRead(ActivityRecord); VerifierStat->OnFileProgress(BuildFile, FileReader->Tell()); ThreadProcessedBytes.fetch_add(ActivityRecord.Size, std::memory_order_release); } HashState.Final(); HashState.GetHash(HashValue.Hash); if (HashValue == BuildFileManifest.FileHash) { VerifyResult = EVerifyResult::Success; } else if (!bShouldAbort) { VerifyResult = EVerifyResult::HashCheckFailed; } else { VerifyResult = EVerifyResult::Aborted; } } FileReader->Close(); } else if (FileSystem->FileExists(*FileToVerify)) { VerifyResult = EVerifyResult::OpenFileFailed; } else { VerifyResult = EVerifyResult::FileMissing; } if (VerifyResult != EVerifyResult::Success) { VerifierStat->OnFileProgress(BuildFile, BuildFileManifest.FileSize); } return VerifyResult; } EVerifyResult FVerifier::VerfiyFileSize(const FString& BuildFile, const FFileManifest& BuildFileManifest) { // Pause if necessary. while (bIsPaused && !bShouldAbort) { FPlatformProcess::Sleep(0.1f); } VerifierStat->OnFileProgress(BuildFile, 0); int64 FileSize; EVerifyResult VerifyResult; if (FileSystem->GetFileSize(*SelectFullFilePath(BuildFile), FileSize)) { if (FileSize == BuildFileManifest.FileSize) { VerifyResult = EVerifyResult::Success; } else { VerifyResult = EVerifyResult::FileSizeFailed; } } else { VerifyResult = EVerifyResult::FileMissing; } VerifierStat->OnFileProgress(BuildFile, BuildFileManifest.FileSize); ThreadProcessedBytes.fetch_add(BuildFileManifest.FileSize, std::memory_order_release); return VerifyResult; } IVerifier* FVerifierFactory::Create(IFileSystem* FileSystem, IVerifierStat* VerifierStat, EVerifyMode VerifyMode, IBuildInstallerSharedContextPtr SharedContext, IBuildManifestSet* ManifestSet, FString VerifyDirectory, FString StagedFileDirectory) { check(FileSystem != nullptr); check(VerifierStat != nullptr); return new FVerifier(FileSystem, VerifierStat, VerifyMode, SharedContext, ManifestSet, MoveTemp(VerifyDirectory), MoveTemp(StagedFileDirectory)); } }