// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "HAL/Event.h" #include "HAL/FileManager.h" #include "HAL/PlatformFile.h" #include "HAL/PlatformFileManager.h" #include "HAL/Runnable.h" #include "Interfaces/IBuildManifest.h" #include "Interfaces/IBuildPatchServicesModule.h" #include "Misc/CoreDelegates.h" #include "Misc/Paths.h" #include "Stats/Stats.h" // Helper class to find all pak/mainfests files. class FFileSearchVisitor : public IPlatformFile::FDirectoryVisitor { FString FileWildcard; TArray& FoundFiles; public: FFileSearchVisitor(FString InFileWildcard, TArray& InFoundFiles) : FileWildcard(InFileWildcard) , FoundFiles(InFoundFiles) {} virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) { if (bIsDirectory == false) { FString Filename(FilenameOrDirectory); if (Filename.MatchesWildcard(FileWildcard)) { FoundFiles.Add(Filename); } } return true; } }; class IBuildPatchServicesModule; class FChunkSetupTask : public FRunnable, public IPlatformFile::FDirectoryVisitor { public: /** Input parameters */ IBuildPatchServicesModule* BPSModule; FString InstallDir; // Intermediate directory where installed chunks may be waiting FString ContentDir; // Directory where installed chunks need to live to be mounted FString HoldingDir; // Directory where manifest for chunks that are out of date and can be use for updates but not mounted. const TArray* CurrentMountPaks; // Array of already mounted Paks /** Output */ FEvent* CompleteEvent; TArray MountedPaks; TMultiMap InstalledChunks; TMultiMap HoldingChunks; /** Working */ TArray FoundPaks; TArray FoundManifests; FFileSearchVisitor PakVisitor; FFileSearchVisitor ManifestVisitor; TArray ManifestsToRemove; uint32 Pass; FChunkSetupTask() : BPSModule(nullptr) , CurrentMountPaks(nullptr) , PakVisitor(TEXT("*.pak"), FoundPaks) , ManifestVisitor(TEXT("*.manifest"), FoundManifests) { CompleteEvent = FPlatformProcess::GetSynchEventFromPool(true); } virtual ~FChunkSetupTask() { FPlatformProcess::ReturnSynchEventToPool(CompleteEvent); } void SetupWork(IBuildPatchServicesModule* InBPSModule, FString InInstallDir, FString InContentDir, FString InHoldingDir, const TArray& InCurrentMountedPaks) { BPSModule = InBPSModule; InstallDir = InInstallDir; ContentDir = InContentDir; HoldingDir = InHoldingDir; CurrentMountPaks = &InCurrentMountedPaks; Pass = 0; InstalledChunks.Reset(); MountedPaks.Reset(); FoundManifests.Reset(); FoundPaks.Reset(); ManifestsToRemove.Reset(); CompleteEvent->Reset(); } void DoWork() { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); PlatformFile.IterateDirectory(*InstallDir, *this); for (const auto& ToRemove : ManifestsToRemove) { PlatformFile.DeleteFile(*ToRemove); } ++Pass; PlatformFile.IterateDirectory(*ContentDir, *this); ++Pass; PlatformFile.IterateDirectory(*HoldingDir, *this); CompleteEvent->Trigger(); } bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override { if (!bIsDirectory) { return true; } IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); FoundManifests.Reset(); PlatformFile.IterateDirectory(FilenameOrDirectory, ManifestVisitor); if (FoundManifests.Num() == 0) { return true; } // Only allow one manifest per folder, any more suggests corruption so mark the folder for delete if (FoundManifests.Num() > 1) { ManifestsToRemove.Append(FoundManifests); return true; } // Load the manifest, so that can be classed as installed auto Manifest = BPSModule->LoadManifestFromFile(FoundManifests[0]); if (!Manifest.IsValid()) { //Something is wrong, suggests corruption so mark the folder for delete ManifestsToRemove.Append(FoundManifests); return true; } auto ChunkIDField = Manifest->GetCustomField(TEXT("ChunkID")); if (!ChunkIDField.IsValid()) { //Something is wrong, suggests corruption so mark the folder for delete ManifestsToRemove.Append(FoundManifests); return true; } auto ChunkPatchField = Manifest->GetCustomField(TEXT("bIsPatch")); bool bIsPatch = ChunkPatchField.IsValid() ? ChunkPatchField->AsString() == TEXT("true") : false; uint32 ChunkID = (uint32)ChunkIDField->AsInteger(); if (Pass == 1) { InstalledChunks.AddUnique(ChunkID, Manifest); } else if (Pass == 0 && ContentDir != InstallDir) { FString ChunkFdrName = FString::Printf(TEXT("%s%d"), !bIsPatch ? TEXT("base") : TEXT("patch"), ChunkID); FString DestDir = *FPaths::Combine(*ContentDir, *ChunkFdrName); if (PlatformFile.DirectoryExists(*DestDir)) { PlatformFile.DeleteDirectoryRecursively(*DestDir); } PlatformFile.CreateDirectoryTree(*DestDir); if (PlatformFile.CopyDirectoryTree(*DestDir, FilenameOrDirectory, true)) { ManifestsToRemove.Add(FilenameOrDirectory); } } else if (Pass == 2) { HoldingChunks.AddUnique(ChunkID, Manifest); } return true; } static const TCHAR *Name() { return TEXT("FChunkSetup"); } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FChunkSetupTask, STATGROUP_ThreadPoolAsyncTasks); } uint32 Run() { DoWork(); return 0; } bool IsDone() { return CompleteEvent->Wait(FTimespan::Zero()); } }; class FChunkMountTask : public FRunnable, public IPlatformFile::FDirectoryVisitor { public: /** Input parameters */ IBuildPatchServicesModule* BPSModule; FString ContentDir; // Directory where installed chunks need to live to be mounted const TArray* CurrentMountPaks; // Array of already mounted Paks const TSet* ExpectedChunks; //Manifests expected to be seen. Chunks not in this list are deleted /** Output */ FEvent* CompleteEvent; TArray MountedPaks; /** Working */ TArray FoundPaks; TArray FoundManifests; TArray ChunkInstallToDestroy; FFileSearchVisitor PakVisitor; FFileSearchVisitor ManifestVisitor; FChunkMountTask() : BPSModule(nullptr) , CurrentMountPaks(nullptr) , PakVisitor(TEXT("*.pak"), FoundPaks) , ManifestVisitor(TEXT("*.manifest"), FoundManifests) { CompleteEvent = FPlatformProcess::GetSynchEventFromPool(true); } ~FChunkMountTask() { FPlatformProcess::ReturnSynchEventToPool(CompleteEvent); } void SetupWork(IBuildPatchServicesModule* InBPSModule, FString InContentDir, const TArray& InCurrentMountedPaks, const TSet& InExpectedChunks) { BPSModule = InBPSModule; ContentDir = InContentDir; CurrentMountPaks = &InCurrentMountedPaks; ExpectedChunks = &InExpectedChunks; MountedPaks.Reset(); FoundManifests.Reset(); FoundPaks.Reset(); ChunkInstallToDestroy.Reset(); CompleteEvent->Reset(); } void DoWork() { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); PlatformFile.IterateDirectory(*ContentDir, *this); for (const auto& Dir : ChunkInstallToDestroy) { PlatformFile.DeleteDirectoryRecursively(*Dir); } CompleteEvent->Trigger(); } bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override { if (!bIsDirectory) { return true; } IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); FoundManifests.Reset(); PlatformFile.IterateDirectory(FilenameOrDirectory, ManifestVisitor); if (FoundManifests.Num() == 0) { return true; } // Only allow one manifest per folder, any more suggests corruption so ignore if (FoundManifests.Num() > 1) { return true; } // Load the manifest, so that can be classed as installed auto Manifest = BPSModule->LoadManifestFromFile(FoundManifests[0]); if (!Manifest.IsValid()) { //Something is wrong, suggests corruption so ignore return true; } auto ChunkIDField = Manifest->GetCustomField(TEXT("ChunkID")); if (!ChunkIDField.IsValid()) { //Something is wrong, suggests corruption so ignore return true; } if (!ExpectedChunks->Find(ChunkIDField->AsInteger())) { //Add this to the list of chunks to remove ChunkInstallToDestroy.Add(FilenameOrDirectory); return true; } FoundPaks.Reset(); PlatformFile.IterateDirectoryRecursively(FilenameOrDirectory, PakVisitor); if (FoundPaks.Num() == 0) { return true; } auto PakReadOrderField = Manifest->GetCustomField(TEXT("PakReadOrdering")); uint32 PakReadOrder = PakReadOrderField.IsValid() ? (uint32)PakReadOrderField->AsInteger() : 0; for (const auto& PakPath : FoundPaks) { //Note: A side effect here is that any unmounted paks get mounted. This is desirable as // any previously installed chunks get mounted if (!CurrentMountPaks->Contains(PakPath) && !MountedPaks.Contains(PakPath)) { if (FCoreDelegates::MountPak.IsBound()) { auto bSuccess = FCoreDelegates::MountPak.Execute(PakPath, PakReadOrder); #if !UE_BUILD_SHIPPING if (!bSuccess) { // This can fail because of the sandbox system - which the pak system doesn't understand. auto SandboxedPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*PakPath); bSuccess = FCoreDelegates::MountPak.Execute(SandboxedPath, PakReadOrder); } #endif //Register the install BPSModule->RegisterAppInstallation(Manifest.ToSharedRef(), FilenameOrDirectory); } } } return true; } static const TCHAR *Name() { return TEXT("FChunkSetup"); } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FChunkMountTask, STATGROUP_ThreadPoolAsyncTasks); } uint32 Run() { DoWork(); return 0; } bool IsDone() { return CompleteEvent->Wait(FTimespan::Zero()); } };