Files
UnrealEngineUWP/Engine/Source/Developer/Virtualization/Private/PackageSubmissionChecks.cpp
paul chipchase b2be464942 Remove the check for FPackageTrailer::IsEnabled when virtualizing payloads. We don't care if the system is currently disabled, only what the package file itself contains.
#rb trivial
#rnx
#preflight 623c5912c3399da9533ffd30

- The package trailer will be on by default soon and the option to remove it disabled, so this check is a bit pointless anyway.

[CL 19493442 by paul chipchase in ue5-main branch]
2022-03-24 08:17:01 -04:00

476 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PackageSubmissionChecks.h"
#include "Containers/UnrealString.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/PlatformTime.h"
#include "Internationalization/Internationalization.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "Misc/ScopedSlowTask.h"
#include "Serialization/EditorBulkData.h"
#include "UObject/Linker.h"
#include "UObject/Package.h"
#include "UObject/PackageResourceManager.h"
#include "UObject/PackageTrailer.h"
#include "UObject/UObjectGlobals.h"
#include "Virtualization/VirtualizationSystem.h"
#define LOCTEXT_NAMESPACE "Virtualization"
// When enabled we will validate truncated packages right after the truncation process to
// make sure that the package format is still correct once the package trailer has been
// removed.
#define UE_VALIDATE_TRUNCATED_PACKAGE 1
// When enabled we will check the payloads to see if they already exist in the persistent storage
// backends before trying to push them.
#define UE_PRECHECK_PAYLOAD_STATUS 1
namespace UE::Virtualization
{
/**
* Check that the given package ends with PACKAGE_FILE_TAG. Intended to be used to make sure that
* we have truncated a package correctly when removing the trailers.
*
* @param PackagePath The path of the package that should be checked
* @param Errors [out] Errors created by the function will be added here
*
* @return True if the package is correctly terminated with a PACKAGE_FILE_TAG, false if the tag
* was not found or if we were unable to read the file's contents.
*/
bool ValidatePackage(const FString& PackagePath, TArray<FText>& Errors)
{
TUniquePtr<IFileHandle> TempFileHandle(FPlatformFileManager::Get().GetPlatformFile().OpenRead(*PackagePath));
if (!TempFileHandle.IsValid())
{
FText ErrorMsg = FText::Format(LOCTEXT("Virtualization_OpenValidationFailed", "Unable to open '{0}' so that it can be validated"),
FText::FromString(PackagePath));
Errors.Add(ErrorMsg);
return false;
}
TempFileHandle->SeekFromEnd(-4);
uint32 PackageTag = INDEX_NONE;
if (!TempFileHandle->Read((uint8*)&PackageTag, 4) || PackageTag != PACKAGE_FILE_TAG)
{
FText ErrorMsg = FText::Format(LOCTEXT("Virtualization_ValidationFailed", "The package '{0}' does not end with a valid tag, the file is considered corrupt"),
FText::FromString(PackagePath));
Errors.Add(ErrorMsg);
return false;
}
return true;
}
/**
* Creates a copy of the given package but the copy will not include the FPackageTrailer.
*
* @param PackagePath The path of the package to copy
* @param CopyPath The path where the copy should be created
* @param Trailer The trailer found in 'PackagePath' that is already loaded
* @param Errors [out] Errors created by the function will be added here
*
* @return Returns true if the package was copied correctly, false otherwise. Note even when returning false a file might have been created at 'CopyPath'
*/
bool TryCopyPackageWithoutTrailer(const FPackagePath PackagePath, const FString& CopyPath, const FPackageTrailer& Trailer, TArray<FText>& Errors)
{
// TODO: Consider adding a custom copy routine to only copy the data we want, rather than copying the full file then truncating
const FString PackageFilePath = PackagePath.GetLocalFullPath();
if (IFileManager::Get().Copy(*CopyPath, *PackageFilePath) != ECopyResult::COPY_OK)
{
FText Message = FText::Format( LOCTEXT("Virtualization_CopyFailed", "Unable to copy package file '{0}' for virtualization"),
FText::FromString(PackagePath.GetDebugName()));
Errors.Add(Message);
return false;
}
const int64 PackageSizeWithoutTrailer = IFileManager::Get().FileSize(*PackageFilePath) - Trailer.GetTrailerLength();
{
TUniquePtr<IFileHandle> TempFileHandle(FPlatformFileManager::Get().GetPlatformFile().OpenWrite(*CopyPath, true));
if (!TempFileHandle.IsValid())
{
FText Message = FText::Format(LOCTEXT("Virtualization_TruncOpenFailed", "Failed to open package file for truncation'{0}' when virtualizing"),
FText::FromString(CopyPath));
Errors.Add(Message);
return false;
}
if (!TempFileHandle->Truncate(PackageSizeWithoutTrailer))
{
FText Message = FText::Format(LOCTEXT("Virtualization_TruncFailed", "Failed to truncate '{0}' when virtualizing"),
FText::FromString(CopyPath));
Errors.Add(Message);
return false;
}
}
#if UE_VALIDATE_TRUNCATED_PACKAGE
// Validate we didn't break the package
if (!ValidatePackage(CopyPath, Errors))
{
return false;
}
#endif //UE_VALIDATE_TRUNCATED_PACKAGE
return true;
}
void VirtualizePackages(const TArray<FString>& FilesToSubmit, TArray<FText>& OutDescriptionTags, TArray<FText>& OutErrors)
{
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::VirtualizePackages);
IVirtualizationSystem& System = IVirtualizationSystem::Get();
// TODO: We could check to see if the package is virtualized even if it is disabled for the project
// as a safety feature?
if (!System.IsEnabled())
{
return;
}
if (!System.IsPushingEnabled(EStorageType::Persistent))
{
UE_LOG(LogVirtualization, Verbose, TEXT("Pushing to persistent backend storage is disabled"));
return;
}
const double StartTime = FPlatformTime::Seconds();
FScopedSlowTask Progress(5.0f, LOCTEXT("Virtualization_Task", "Virtualizing Assets..."));
Progress.MakeDialog();
// Other systems may have added errors to this array, we need to check so later we can determine if this function added any additional errors.
const int32 NumErrors = OutErrors.Num();
struct FPackageInfo
{
FPackagePath Path;
FPackageTrailer Trailer;
TArray<FIoHash> LocalPayloads;
int32 PayloadIndex = INDEX_NONE;
bool bWasTrailerUpdated = false;
};
UE_LOG(LogVirtualization, Display, TEXT("Considering %d file(s) for virtualization"), FilesToSubmit.Num());
TArray<FPackageInfo> Packages;
Packages.Reserve(FilesToSubmit.Num());
TArray<FIoHash> AllLocalPayloads;
AllLocalPayloads.Reserve(FilesToSubmit.Num());
Progress.EnterProgressFrame(1.0f);
// From the list of files to submit we need to find all of the valid packages that contain
// local payloads that need to be virtualized.
int64 TotalPayloadsToCheck = 0;
for (const FString& AbsoluteFilePath : FilesToSubmit)
{
FPackagePath PackagePath = FPackagePath::FromLocalPath(AbsoluteFilePath);
// TODO: How to handle text packages?
if (FPackageName::IsPackageExtension(PackagePath.GetHeaderExtension()) || FPackageName::IsTextPackageExtension(PackagePath.GetHeaderExtension()))
{
FPackageTrailer Trailer;
if (FPackageTrailer::TryLoadFromPackage(PackagePath, Trailer))
{
// The following is not expected to ever happen, currently we give a user facing error but it generally means that the asset is broken somehow.
ensureMsgf(Trailer.GetNumPayloads(EPayloadFilter::Referenced) == 0, TEXT("Trying to virtualize a package that already contains payload references which the workspace file should not ever contain!"));
if (Trailer.GetNumPayloads(EPayloadFilter::Referenced) > 0)
{
FText Message = FText::Format( LOCTEXT("Virtualization_PkgHasReferences", "Cannot virtualize the package '{1}' as it has referenced payloads in the trailer"),
FText::FromString(PackagePath.GetDebugName()));
OutErrors.Add(Message);
return;
}
FPackageInfo PkgInfo;
PkgInfo.Path = MoveTemp(PackagePath);
PkgInfo.Trailer = MoveTemp(Trailer);
PkgInfo.LocalPayloads = PkgInfo.Trailer.GetPayloads(EPayloadFilter::Local);
TotalPayloadsToCheck += PkgInfo.LocalPayloads.Num();
if (!PkgInfo.LocalPayloads.IsEmpty())
{
PkgInfo.PayloadIndex = AllLocalPayloads.Num();
AllLocalPayloads.Append(PkgInfo.LocalPayloads);
Packages.Emplace(MoveTemp(PkgInfo));
}
}
}
}
UE_LOG(LogVirtualization, Display, TEXT("Found %" INT64_FMT " payload(s) in %d package(s) that need to be examined for virtualization"), TotalPayloadsToCheck, Packages.Num());
Progress.EnterProgressFrame(1.0f);
TArray<FPayloadStatus> PayloadStatuses;
if (System.QueryPayloadStatuses(AllLocalPayloads, EStorageType::Persistent, PayloadStatuses) != EQueryResult::Success)
{
FText Message = LOCTEXT("Virtualization_DoesExistFail", "Failed to find the status of the payloads in the packages being submitted");
OutErrors.Add(Message);
return;
}
// Update payloads that are already in persistent storage and don't need to be pushed
int64 TotalPayloadsToVirtualize = 0;
for (FPackageInfo& PackageInfo : Packages)
{
check(PackageInfo.LocalPayloads.IsEmpty() || PackageInfo.PayloadIndex != INDEX_NONE); // If we have payloads we should have an index
#if UE_PRECHECK_PAYLOAD_STATUS
for (int32 Index = 0; Index < PackageInfo.LocalPayloads.Num(); ++Index)
{
if (PayloadStatuses[PackageInfo.PayloadIndex + Index] == FPayloadStatus::FoundAll)
{
if (PackageInfo.Trailer.UpdatePayloadAsVirtualized(PackageInfo.LocalPayloads[Index]))
{
PackageInfo.bWasTrailerUpdated = true;
}
else
{
FText Message = FText::Format( LOCTEXT("Virtualization_UpdateStatusFailed", "Unable to update the status for the payload '{0}' in the package '{1}'"),
FText::FromString(LexToString(PackageInfo.LocalPayloads[Index])),
FText::FromString(PackageInfo.Path.GetDebugName()));
OutErrors.Add(Message);
return;
}
}
}
// If we made changes we should recalculate the local payloads left
if (PackageInfo.bWasTrailerUpdated)
{
PackageInfo.LocalPayloads = PackageInfo.Trailer.GetPayloads(EPayloadFilter::Local);
}
#endif
PackageInfo.PayloadIndex = INDEX_NONE;
TotalPayloadsToVirtualize += PackageInfo.LocalPayloads.Num();
}
UE_LOG(LogVirtualization, Display, TEXT("Found %" INT64_FMT " payload(s) that potentially need to be pushed to persistent virtualized storage"), TotalPayloadsToVirtualize);
// TODO Optimization: In theory we could have many packages sharing the same payload and we only need to push once
Progress.EnterProgressFrame(1.0f);
TArray<Virtualization::FPushRequest> PayloadsToSubmit;
PayloadsToSubmit.Reserve(TotalPayloadsToVirtualize);
// Push any remaining local payload to the persistent backends
for (FPackageInfo& PackageInfo : Packages)
{
if (PackageInfo.LocalPayloads.IsEmpty())
{
continue;
}
TUniquePtr<FArchive> PackageAr = IPackageResourceManager::Get().OpenReadExternalResource(EPackageExternalResource::WorkspaceDomainFile, PackageInfo.Path.GetPackageName());
if (!PackageAr.IsValid())
{
FText Message = FText::Format( LOCTEXT("Virtualization_PkgOpen", "Failed to open the package '{1}' for reading"),
FText::FromString(PackageInfo.Path.GetDebugName()));
OutErrors.Add(Message);
return;
}
PackageInfo.PayloadIndex = PayloadsToSubmit.Num();
for (const FIoHash& PayloadId : PackageInfo.LocalPayloads)
{
checkf(!PayloadId.IsZero(), TEXT("PackageTrailer for package '%s' should not contain invalid FIoHashs"), *PackageInfo.Path.GetDebugName());
FCompressedBuffer Payload = PackageInfo.Trailer.LoadLocalPayload(PayloadId, *PackageAr);
if (PayloadId != FIoHash(Payload.GetRawHash()))
{
FText Message = FText::Format( LOCTEXT("Virtualization_WrongPayload", "Package {0} loaded an incorrect payload from the trailer. Expected '{1}' Loaded '{2}'"),
FText::FromString(PackageInfo.Path.GetDebugName()),
FText::FromString(LexToString(PayloadId)),
FText::FromString(LexToString(Payload.GetRawHash())));
OutErrors.Add(Message);
return;
}
if (!Payload)
{
FText Message = FText::Format( LOCTEXT("Virtualization_MissingPayload", "Unable to find the payload '{0}' in the local storage of package '{1}'"),
FText::FromString(LexToString(PayloadId)),
FText::FromString(PackageInfo.Path.GetDebugName()));
OutErrors.Add(Message);
return;
}
PayloadsToSubmit.Emplace(PayloadId, MoveTemp(Payload), PackageInfo.Path.GetDebugName());
}
}
Progress.EnterProgressFrame(1.0f);
if (!System.PushData(PayloadsToSubmit, EStorageType::Persistent))
{
FText Message = LOCTEXT("Virtualization_PushFailure", "Failed to push payloads");
OutErrors.Add(Message);
return;
}
int64 TotalPayloadsVirtualized = 0;
for (const Virtualization::FPushRequest& Request : PayloadsToSubmit)
{
TotalPayloadsVirtualized += Request.Status == FPushRequest::EStatus::Success ? 1 : 0;
}
UE_LOG(LogVirtualization, Display, TEXT("Pushed %" INT64_FMT " payload(s) to persistent virtualized storage"), TotalPayloadsVirtualized);
// Update the package info for the submitted payloads
for (FPackageInfo& PackageInfo : Packages)
{
for (int32 Index = 0; Index < PackageInfo.LocalPayloads.Num(); ++Index)
{
const Virtualization::FPushRequest& Request = PayloadsToSubmit[PackageInfo.PayloadIndex + Index];
check(Request.Identifier == PackageInfo.LocalPayloads[Index]);
if (Request.Status == Virtualization::FPushRequest::EStatus::Success)
{
if (PackageInfo.Trailer.UpdatePayloadAsVirtualized(Request.Identifier))
{
PackageInfo.bWasTrailerUpdated = true;
}
else
{
FText Message = FText::Format( LOCTEXT("Virtualization_UpdateStatusFailed", "Unable to update the status for the payload '{0}' in the package '{1}'"),
FText::FromString(LexToString(Request.Identifier)),
FText::FromString(PackageInfo.Path.GetDebugName()));
OutErrors.Add(Message);
return;
}
}
}
}
Progress.EnterProgressFrame(1.0f);
TArray<TPair<FPackagePath, FString>> PackagesToReplace;
// Any package with an updated trailer needs to be copied and an updated trailer appended
for (FPackageInfo& PackageInfo : Packages)
{
if (!PackageInfo.bWasTrailerUpdated)
{
continue;
}
const FPackagePath& PackagePath = PackageInfo.Path; // No need to validate path, we checked this earlier
const FString PackageFilePath = PackagePath.GetLocalFullPath();
const FString BaseName = FPaths::GetBaseFilename(PackagePath.GetPackageName());
const FString TempFilePath = FPaths::CreateTempFilename(*FPaths::ProjectSavedDir(), *BaseName.Left(32));
// TODO Optimization: Combine TryCopyPackageWithoutTrailer with the appending of the new trailer to avoid opening multiple handles
// Create copy of package minus the trailer the trailer
if (!TryCopyPackageWithoutTrailer(PackagePath, TempFilePath, PackageInfo.Trailer, OutErrors))
{
return;
}
TUniquePtr<FArchive> PackageAr = IPackageResourceManager::Get().OpenReadExternalResource(EPackageExternalResource::WorkspaceDomainFile, PackagePath.GetPackageName());
if (!PackageAr.IsValid())
{
FText Message = FText::Format( LOCTEXT("Virtualization_PkgOpen", "Failed to open the package '{1}' for reading"),
FText::FromString(PackagePath.GetDebugName()));
OutErrors.Add(Message);
return;
}
TUniquePtr<FArchive> CopyAr(IFileManager::Get().CreateFileWriter(*TempFilePath, EFileWrite::FILEWRITE_Append));
if (!CopyAr.IsValid())
{
FText Message = FText::Format( LOCTEXT("Virtualization_TrailerAppendOpen", "Unable to open '{0}' to append the trailer'"),
FText::FromString(TempFilePath));
OutErrors.Add(Message);
return;
}
FPackageTrailerBuilder TrailerBuilder = FPackageTrailerBuilder::CreateFromTrailer(PackageInfo.Trailer, *PackageAr, PackagePath.GetPackageFName());
if (!TrailerBuilder.BuildAndAppendTrailer(nullptr, *CopyAr))
{
FText Message = FText::Format( LOCTEXT("Virtualization_TrailerAppend", "Failed to append the trailer to '{0}'"),
FText::FromString(TempFilePath));
OutErrors.Add(Message);
return;
}
// Now that we have successfully created a new version of the package with an updated trailer
// we need to mark that it should replace the original package.
PackagesToReplace.Emplace(PackagePath, TempFilePath);
}
UE_LOG(LogVirtualization, Display, TEXT("%d package(s) had their trailer container modified and need to be updated"), PackagesToReplace.Num());
if (NumErrors == OutErrors.Num())
{
// TODO: Consider using the SavePackage model (move the original, then replace, so we can restore all of the original packages if needed)
// having said that, once a package is in PackagesToReplace it should still be safe to submit so maybe we don't need this level of protection?
// We need to reset the loader of any package that we want to re-save over
for (const TPair<FPackagePath, FString>& Iterator : PackagesToReplace)
{
UPackage* Package = FindObjectFast<UPackage>(nullptr, Iterator.Key.GetPackageFName());
if (Package != nullptr)
{
ResetLoadersForSave(Package, *Iterator.Key.GetLocalFullPath());
}
}
// Since we had no errors we can now replace all of the packages that were virtualized data with the virtualized replacement file.
for(const TPair<FPackagePath,FString>& Iterator : PackagesToReplace)
{
const FString OriginalPackagePath = Iterator.Key.GetLocalFullPath();
const FString& NewPackagePath = Iterator.Value;
if (!IFileManager::Get().Move(*OriginalPackagePath, *NewPackagePath))
{
FText Message = FText::Format( LOCTEXT("Virtualization_MoveFailed", "Unable to replace the package '{0}' with the virtualized version"),
FText::FromString(Iterator.Key.GetDebugName()));
OutErrors.Add(Message);
continue;
}
}
}
// If we had no new errors add the validation tag to indicate that the packages are safe for submission.
// TODO: Currently this is a simple tag to make it easier for us to track which assets were submitted via the
// virtualization process in a test project. This should be expanded when we add proper p4 server triggers.
if (NumErrors == OutErrors.Num())
{
FText Tag = FText::FromString(TEXT("#virtualized"));
OutDescriptionTags.Add(Tag);
}
const double TimeInSeconds = FPlatformTime::Seconds() - StartTime;
UE_LOG(LogVirtualization, Verbose, TEXT("Virtualization pre submit check took %.3f(s)"), TimeInSeconds);
}
} // namespace UE::Virtualization
#undef LOCTEXT_NAMESPACE