You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#rb CarlMagnus.Nordin #rnx #tests Ran overnight preflights on several platforms, opened/cooked/staged/ran the oldest version of InfiltratorDemo that can be downloaded (4.11) ### ObjectVersion - Add a new version enum EUnrealEngineObjectUE5Version. -- This version number starts at 1000 which leaves more than enough for for EUnrealEngineObjectUEVersion to be expanded - Even though very few changes (if any at all) to EUnrealEngineObjectUE4Version are expected there is a static assert to make sure that EUnrealEngineObjectUEVersion::AUTOMATIC_VERSION never overtakes EUnrealEngineObjectUE5Version::INITIAL_VERSION. - Add a struct FPackageFileVersion that wraps around the version numbers and is used to store them instead of raw int32 values which was done before. This should make it easier to add new version numbers in the future if we desire (although this will cause problems in places that serialize the struct directly) ### FPackageFileSummary - Adding a new entry to CurrentLegacyFileVersion at value -8 which shows the UE5 version being added. This lets us make the changes without needing to submit anything to UE4 Main. - When loading a package that does not have a UE5 version, it will remain at 0. - Added ::IsFileVersionTooOld and ::IsFileVersionTooNew to replace hardcoded tests in the code base for version validity. This will make it easier to make changes in the future. - A few months ago most of the accessors of the version number were deprecated in favour of a version that did not contain the Engine number (ie UE4Ver -> UEVer in Archive) but to work with these changes the renamed methods now will return or accept the version as FPackageFileVersion rather than int32. The old UE4 methods will remain deprecated and direct licensees to use the new methods. ### Archive - Now stores the version as a FPackageFileVersion rather than int32 ### LinkerLoad - Reports the larger version number if we detect a higher version number than we support. Note that this could cause an issue if the UE4 version is ever raised but helps keep the code simple. ### AssetData - Need to add a new version here to manage existing data that only has the UE4 version ### EditorDomain - We do not need to version the format, we can just invalidate existing editor domain entries via EditorDomainVersion ### EditorServer - When reporting that a package is too old we report the UE4 version as that is the only version that can be older than VER_UE4_OLDEST_LOADABLE_PACKAGE - When reporting that a package is too new it can be either the UE4 or the UE5 version so we print them together "UE4Ver|UE5Ver" ### ContentCommandlets - The min and max resave versions have been kept as a single value, you will not be able to resave against different UE4 and UE5 versions at the same time. It doesn't seem like a useful feature and would greatly increase the complexity of the code. - We will also only report the file version as a single value. ### ManifestUObject - This class was setting an older obsolete version on purpose to try and maintain compatibility with older clients so we need to provide a way to create an older UE4 only version that will leave the UE5 version as unset. ### NetworkPlatformFile - I was unable to test the code path in FNetworkPlatformFile::ProcessServerCachedFilesResponse as I am unsure how to run the game in a mode that will actually use it. - When reading an older "CookedVersion.txt" that was saved with a single version, the reads will fail and this will count as a version change in the code so that all of the existing files will be deleted. The existing code would not give the user a log message when this happens and given the very small time window where this might happen caused by this change I have opted to leave this alone and not add any additional logging. - If we do detect a version mismatch we will still only log the version number as a single version. ### CookOnTheFlyServer - We now add each version number to the IniVersionMap rather than merge the version and license version as a key/value pair. This allows us to a) use both the UE4 and UE5 version numbers b) we now log a warning that the version values don't match when it is changed, previously since it was a key value we would log a warning about an additional setting instead. -- I also added "vs" to the log message when values are mismatched to make the space between the two values being printed clearer. #ROBOMERGE-OWNER: paul.chipchase #ROBOMERGE-AUTHOR: paul.chipchase #ROBOMERGE-SOURCE: CL 17549459 via CL 17550236 via CL 17550238 via CL 17550582 #ROBOMERGE-BOT: STARSHIP (Release-Engine-Staging -> Release-Engine-Test) (v870-17433530) #ROBOMERGE[STARSHIP]: UE5-Main [CL 17550583 by paul chipchase in ue5-release-engine-test branch]
1381 lines
44 KiB
C++
1381 lines
44 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "NetworkFileServerConnection.h"
|
|
#include "HAL/PlatformFileManager.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/ScopeLock.h"
|
|
#include "Serialization/BufferArchive.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/LocalTimestampDirectoryVisitor.h"
|
|
#include "IPlatformFileSandboxWrapper.h"
|
|
#include "NetworkMessage.h"
|
|
#include "ProjectDescriptor.h"
|
|
#include "NetworkFileSystemLog.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "Interfaces/ITargetPlatform.h"
|
|
#include "HAL/PlatformTime.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
|
|
|
|
/**
|
|
* Helper function for resolving engine and game sandbox paths
|
|
*/
|
|
void GetSandboxRootDirectories(FSandboxPlatformFile* Sandbox, FString& SandboxEngine, FString& SandboxProject, FString& SandboxEnginePlatformExtensions, FString& SandboxProjectPlatformExtensions, const FString& LocalEngineDir, const FString& LocalProjectDir, const FString& LocalEnginePlatformExtensionsDir, const FString& LocalProjectPlatformExtensionsDir)
|
|
{
|
|
SandboxEngine = Sandbox->ConvertToSandboxPath(*LocalEngineDir);
|
|
if (SandboxEngine.EndsWith(TEXT("/"), ESearchCase::CaseSensitive) == false)
|
|
{
|
|
SandboxEngine += TEXT("/");
|
|
}
|
|
|
|
// we need to add an extra bit to the game path to make the sandbox convert it correctly (investigate?)
|
|
// @todo: double check this
|
|
SandboxProject = Sandbox->ConvertToSandboxPath(*(LocalProjectDir + TEXT("a.txt"))).Replace(TEXT("a.txt"), TEXT(""));
|
|
SandboxEnginePlatformExtensions = Sandbox->ConvertToSandboxPath(*(LocalEnginePlatformExtensionsDir + TEXT("a.txt"))).Replace(TEXT("a.txt"), TEXT(""));
|
|
SandboxProjectPlatformExtensions = Sandbox->ConvertToSandboxPath(*(LocalProjectPlatformExtensionsDir + TEXT("a.txt"))).Replace(TEXT("a.txt"), TEXT(""));
|
|
}
|
|
|
|
static FString MakeAbsoluteNormalizedDir(const FString& InPath)
|
|
{
|
|
FString Out = FPaths::ConvertRelativePathToFull(InPath);
|
|
if (Out.EndsWith(TEXT("/")))
|
|
{
|
|
Out.RemoveAt(Out.Len() - 1, 1, false);
|
|
}
|
|
return Out;
|
|
}
|
|
|
|
struct FSandboxOnlyScope
|
|
{
|
|
FSandboxOnlyScope(FSandboxPlatformFile& InSandbox, bool bInSandboxOnly)
|
|
: Sandbox(InSandbox)
|
|
{
|
|
Sandbox.SetSandboxOnly(bInSandboxOnly);
|
|
}
|
|
|
|
~FSandboxOnlyScope()
|
|
{
|
|
Sandbox.SetSandboxOnly(false);
|
|
}
|
|
|
|
FSandboxPlatformFile& Sandbox;
|
|
};
|
|
|
|
/* FNetworkFileServerClientConnection structors
|
|
*****************************************************************************/
|
|
|
|
FNetworkFileServerClientConnection::FNetworkFileServerClientConnection(const FNetworkFileServerOptions& Options)
|
|
: LastHandleId(0)
|
|
, Sandbox(NULL)
|
|
, NetworkFileDelegates(&Options.Delegates)
|
|
, ActiveTargetPlatforms(Options.TargetPlatforms)
|
|
, bRestrictPackageAssetsToSandbox(Options.bRestrictPackageAssetsToSandbox)
|
|
{
|
|
//stats
|
|
FileRequestDelegateTime = 0.0;
|
|
PackageFileTime = 0.0;
|
|
UnsolicitedFilesTime = 0.0;
|
|
|
|
FileRequestCount = 0;
|
|
UnsolicitedFilesCount = 0;
|
|
PackageRequestsSucceeded = 0;
|
|
PackageRequestsFailed = 0;
|
|
FileBytesSent = 0;
|
|
|
|
if ( NetworkFileDelegates && NetworkFileDelegates->OnFileModifiedCallback )
|
|
{
|
|
NetworkFileDelegates->OnFileModifiedCallback->AddRaw(this, &FNetworkFileServerClientConnection::FileModifiedCallback);
|
|
}
|
|
|
|
LocalEngineDir = FPaths::EngineDir();
|
|
LocalProjectDir = FPaths::ProjectDir();
|
|
LocalEnginePlatformExtensionsDir = FPaths::EnginePlatformExtensionsDir();
|
|
LocalProjectPlatformExtensionsDir = FPaths::ProjectPlatformExtensionsDir();
|
|
|
|
if (FPaths::IsProjectFilePathSet())
|
|
{
|
|
LocalProjectDir = FPaths::GetPath(FPaths::GetProjectFilePath()) + TEXT("/");
|
|
FPaths::MakeStandardFilename(LocalProjectDir);
|
|
}
|
|
|
|
LocalEngineDirAbs = MakeAbsoluteNormalizedDir(LocalEngineDir);
|
|
LocalProjectDirAbs = MakeAbsoluteNormalizedDir(LocalProjectDir);
|
|
LocalEnginePlatformExtensionsDirAbs = MakeAbsoluteNormalizedDir(LocalEnginePlatformExtensionsDir);
|
|
LocalProjectPlatformExtensionsDirAbs = MakeAbsoluteNormalizedDir(LocalEnginePlatformExtensionsDir);
|
|
}
|
|
|
|
|
|
FNetworkFileServerClientConnection::~FNetworkFileServerClientConnection( )
|
|
{
|
|
if (NetworkFileDelegates && NetworkFileDelegates->OnFileModifiedCallback)
|
|
{
|
|
NetworkFileDelegates->OnFileModifiedCallback->RemoveAll(this);
|
|
}
|
|
|
|
// close all the files the client had opened through us when the client disconnects
|
|
for (TMap<uint64, IFileHandle*>::TIterator It(OpenFiles); It; ++It)
|
|
{
|
|
delete It.Value();
|
|
}
|
|
}
|
|
|
|
static bool TrySubstituteDirectory(FString& FilenameToConvert, const FString& Directory, const FString& DirectoryToReplace)
|
|
{
|
|
FString NormalizedFilenameToConvert = FilenameToConvert;
|
|
FPaths::NormalizeFilename(NormalizedFilenameToConvert);
|
|
FString NormalizedDirectoryToReplace = DirectoryToReplace;
|
|
FPaths::NormalizeDirectoryName(NormalizedDirectoryToReplace);
|
|
if (NormalizedFilenameToConvert.StartsWith(NormalizedDirectoryToReplace) && (NormalizedFilenameToConvert.Len() == NormalizedDirectoryToReplace.Len() || NormalizedFilenameToConvert[NormalizedDirectoryToReplace.Len()] == '/'))
|
|
{
|
|
if (NormalizedFilenameToConvert.Len() > NormalizedDirectoryToReplace.Len())
|
|
{
|
|
FilenameToConvert = Directory / NormalizedFilenameToConvert.RightChop(NormalizedDirectoryToReplace.Len() + 1);
|
|
}
|
|
else
|
|
{
|
|
FilenameToConvert = Directory;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* FStreamingNetworkFileServerConnection implementation
|
|
*****************************************************************************/
|
|
void FNetworkFileServerClientConnection::ConvertClientFilenameToServerFilename(FString& FilenameToConvert)
|
|
{
|
|
if (TrySubstituteDirectory(FilenameToConvert, FPaths::EngineDir(), ConnectedEngineDir))
|
|
{
|
|
return;
|
|
}
|
|
if (TrySubstituteDirectory(FilenameToConvert, FPaths::IsProjectFilePathSet() ? LocalProjectDir : (IS_PROGRAM ? ConnectedProjectDir : FPaths::ProjectDir()), ConnectedProjectDir))
|
|
{
|
|
// We have set the replacement value argument of TrySubstituteDirectory to be the same as the search value in the IS_PROGRAM case. We do this because:
|
|
// UnrealFileServer has a ProjectDir of ../../../Engine/Programs/UnrealFileServer.
|
|
// We do *not* want to replace the directory in that case.
|
|
return;
|
|
}
|
|
if (TrySubstituteDirectory(FilenameToConvert, FPaths::EnginePlatformExtensionsDir(), ConnectedEnginePlatformExtensionsDir))
|
|
{
|
|
return;
|
|
}
|
|
if (TrySubstituteDirectory(FilenameToConvert, FPaths::ProjectPlatformExtensionsDir(), ConnectedProjectPlatformExtensionsDir))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
void FNetworkFileServerClientConnection::ConvertLocalFilenameToServerFilename(FString& FilenameToConvert)
|
|
{
|
|
FString FilenameToConvertAbs = FPaths::ConvertRelativePathToFull(FilenameToConvert);
|
|
if (TrySubstituteDirectory(FilenameToConvertAbs, LocalEngineDir, LocalEngineDirAbs))
|
|
{
|
|
FilenameToConvert = FilenameToConvertAbs;
|
|
return;
|
|
}
|
|
if (TrySubstituteDirectory(FilenameToConvertAbs, LocalProjectDir, LocalProjectDirAbs))
|
|
{
|
|
FilenameToConvert = FilenameToConvertAbs;
|
|
return;
|
|
}
|
|
if (TrySubstituteDirectory(FilenameToConvertAbs, LocalEnginePlatformExtensionsDir, LocalEnginePlatformExtensionsDirAbs))
|
|
{
|
|
FilenameToConvert = FilenameToConvertAbs;
|
|
return;
|
|
}
|
|
if (TrySubstituteDirectory(FilenameToConvertAbs, LocalProjectPlatformExtensionsDir, LocalProjectPlatformExtensionsDirAbs))
|
|
{
|
|
FilenameToConvert = FilenameToConvertAbs;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Fixup sandbox paths to match what package loading will request on the client side. e.g.
|
|
* Sandbox path: "../../../Elemental/Content/Elemental/Effects/FX_Snow_Cracks/Crack_02/Materials/M_SnowBlast.uasset ->
|
|
* client path: "../../../Samples/Showcases/Elemental/Content/Elemental/Effects/FX_Snow_Cracks/Crack_02/Materials/M_SnowBlast.uasset"
|
|
* This ensures that devicelocal-cached files will be properly timestamp checked before deletion.
|
|
*/
|
|
TMap<FString, FDateTime> FNetworkFileServerClientConnection::FixupSandboxPathsForClient(const TMap<FString, FDateTime>& SandboxPaths)
|
|
{
|
|
TMap<FString,FDateTime> FixedFiletimes;
|
|
|
|
// since the sandbox remaps from A/B/C to C, and the client has no idea of this, we need to put the files
|
|
// into terms of the actual LocalProjectDir, which is all that the client knows about
|
|
for (TMap<FString, FDateTime>::TConstIterator It(SandboxPaths); It; ++It)
|
|
{
|
|
FixedFiletimes.Add(FixupSandboxPathForClient(It.Key()), It.Value());
|
|
}
|
|
return FixedFiletimes;
|
|
}
|
|
|
|
/**
|
|
* Fixup sandbox paths to match what package loading will request on the client side. e.g.
|
|
* Sandbox path: "../../../Elemental/Content/Elemental/Effects/FX_Snow_Cracks/Crack_02/Materials/M_SnowBlast.uasset ->
|
|
* client path: "../../../Samples/Showcases/Elemental/Content/Elemental/Effects/FX_Snow_Cracks/Crack_02/Materials/M_SnowBlast.uasset"
|
|
* This ensures that devicelocal-cached files will be properly timestamp checked before deletion.
|
|
*/
|
|
FString FNetworkFileServerClientConnection::FixupSandboxPathForClient(const FString& Filename)
|
|
{
|
|
FString Fixed = Sandbox->ConvertToSandboxPath(*Filename);
|
|
Fixed = Fixed.Replace(*SandboxEngine, *LocalEngineDir);
|
|
Fixed = Fixed.Replace(*SandboxProject, *LocalProjectDir);
|
|
Fixed = Fixed.Replace(*SandboxEnginePlatformExtensions, *LocalEnginePlatformExtensionsDir);
|
|
Fixed = Fixed.Replace(*SandboxProjectPlatformExtensions, *LocalProjectPlatformExtensionsDir);
|
|
|
|
if (bSendLowerCase)
|
|
{
|
|
Fixed = Fixed.ToLower();
|
|
}
|
|
return Fixed;
|
|
}
|
|
|
|
static void ConvertServerFilenameToClientFilename(FString& FilenameToConvert, const FString& ConnectedEngineDir, const FString& ConnectedProjectDir)
|
|
{
|
|
|
|
if (FilenameToConvert.StartsWith(FPaths::EngineDir()))
|
|
{
|
|
FilenameToConvert = FilenameToConvert.Replace(*(FPaths::EngineDir()), *ConnectedEngineDir);
|
|
}
|
|
else if (FPaths::IsProjectFilePathSet())
|
|
{
|
|
if (FilenameToConvert.StartsWith(FPaths::GetPath(FPaths::GetProjectFilePath())))
|
|
{
|
|
FilenameToConvert = FilenameToConvert.Replace(*(FPaths::GetPath(FPaths::GetProjectFilePath()) + TEXT("/")), *ConnectedProjectDir);
|
|
}
|
|
}
|
|
#if !IS_PROGRAM
|
|
else if (FilenameToConvert.StartsWith(FPaths::ProjectDir()))
|
|
{
|
|
// UnrealFileServer has a ProjectDir of ../../../Engine/Programs/UnrealFileServer.
|
|
// We do *not* want to replace the directory in that case.
|
|
FilenameToConvert = FilenameToConvert.Replace(*(FPaths::ProjectDir()), *ConnectedProjectDir);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static FCriticalSection SocketCriticalSection;
|
|
|
|
bool FNetworkFileServerClientConnection::ProcessPayload(FArchive& Ar)
|
|
{
|
|
FBufferArchive Out;
|
|
bool Result = true;
|
|
|
|
// first part of the payload is always the command
|
|
uint32 Cmd;
|
|
Ar << Cmd;
|
|
|
|
UE_LOG(LogFileServer, Verbose, TEXT("Processing payload with Cmd %d"), Cmd);
|
|
|
|
// what type of message is this?
|
|
NFS_Messages::Type Msg = NFS_Messages::Type(Cmd);
|
|
|
|
// make sure the first thing is GetFileList which initializes the game/platform
|
|
checkf(Msg == NFS_Messages::GetFileList || Msg == NFS_Messages::Heartbeat || Sandbox != NULL, TEXT("The first client message MUST be GetFileList, not %d"), (int32)Msg);
|
|
|
|
// process the message!
|
|
bool bSendUnsolicitedFiles = false;
|
|
|
|
{
|
|
FScopeLock SocketLock(&SocketCriticalSection);
|
|
|
|
switch (Msg)
|
|
{
|
|
case NFS_Messages::OpenRead:
|
|
ProcessOpenFile(Ar, Out, false);
|
|
break;
|
|
|
|
case NFS_Messages::OpenWrite:
|
|
ProcessOpenFile(Ar, Out, true);
|
|
break;
|
|
|
|
case NFS_Messages::Read:
|
|
ProcessReadFile(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::Write:
|
|
ProcessWriteFile(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::Seek:
|
|
ProcessSeekFile(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::Close:
|
|
ProcessCloseFile(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::MoveFile:
|
|
ProcessMoveFile(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::DeleteFile:
|
|
ProcessDeleteFile(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::GetFileInfo:
|
|
ProcessGetFileInfo(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::CopyFile:
|
|
ProcessCopyFile(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::SetTimeStamp:
|
|
ProcessSetTimeStamp(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::SetReadOnly:
|
|
ProcessSetReadOnly(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::CreateDirectory:
|
|
ProcessCreateDirectory(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::DeleteDirectory:
|
|
ProcessDeleteDirectory(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::DeleteDirectoryRecursively:
|
|
ProcessDeleteDirectoryRecursively(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::ToAbsolutePathForRead:
|
|
ProcessToAbsolutePathForRead(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::ToAbsolutePathForWrite:
|
|
ProcessToAbsolutePathForWrite(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::ReportLocalFiles:
|
|
ProcessReportLocalFiles(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::GetFileList:
|
|
Result = ProcessGetFileList(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::Heartbeat:
|
|
ProcessHeartbeat(Ar, Out);
|
|
break;
|
|
|
|
case NFS_Messages::SyncFile:
|
|
ProcessSyncFile(Ar, Out);
|
|
bSendUnsolicitedFiles = true;
|
|
break;
|
|
|
|
case NFS_Messages::RecompileShaders:
|
|
ProcessRecompileShaders(Ar, Out);
|
|
break;
|
|
|
|
default:
|
|
|
|
UE_LOG(LogFileServer, Error, TEXT("Bad incomming message tag (%d)."), (int32)Msg);
|
|
}
|
|
}
|
|
|
|
|
|
// send back a reply if the command wrote anything back out
|
|
if (Out.Num() && Result )
|
|
{
|
|
int32 NumUnsolictedFiles = 0;
|
|
|
|
|
|
if (bSendUnsolicitedFiles)
|
|
{
|
|
int64 MaxMemoryAllowed = 50 * 1024 * 1024;
|
|
for (const auto& Filename : UnsolictedFiles)
|
|
{
|
|
// get file timestamp and send it to client
|
|
FDateTime ServerTimeStamp = Sandbox->GetTimeStamp(*Filename);
|
|
|
|
TArray<uint8> Contents;
|
|
// open file
|
|
int64 FileSize = Sandbox->FileSize(*Filename);
|
|
|
|
if (MaxMemoryAllowed > FileSize)
|
|
{
|
|
MaxMemoryAllowed -= FileSize;
|
|
++NumUnsolictedFiles;
|
|
}
|
|
}
|
|
Out << NumUnsolictedFiles;
|
|
}
|
|
|
|
UE_LOG(LogFileServer, Verbose, TEXT("Returning payload with %d bytes"), Out.Num());
|
|
|
|
// send back a reply
|
|
Result &= SendPayload( Out );
|
|
|
|
TArray<FString> UnprocessedUnsolictedFiles;
|
|
UnprocessedUnsolictedFiles.Empty(NumUnsolictedFiles);
|
|
|
|
if (bSendUnsolicitedFiles && Result )
|
|
{
|
|
double StartTime;
|
|
StartTime = FPlatformTime::Seconds();
|
|
for (int32 Index = 0; Index < NumUnsolictedFiles; Index++)
|
|
{
|
|
FBufferArchive OutUnsolicitedFile;
|
|
ConvertLocalFilenameToServerFilename(UnsolictedFiles[Index]);
|
|
FString TargetFilename = UnsolictedFiles[Index];
|
|
ConvertServerFilenameToClientFilename(TargetFilename, ConnectedEngineDir, ConnectedProjectDir);
|
|
PackageFile(UnsolictedFiles[Index], TargetFilename, OutUnsolicitedFile);
|
|
|
|
UE_LOG(LogFileServer, Display, TEXT("Returning unsolicited file %s with %d bytes"), *UnsolictedFiles[Index], OutUnsolicitedFile.Num());
|
|
|
|
Result &= SendPayload(OutUnsolicitedFile);
|
|
++UnsolicitedFilesCount;
|
|
}
|
|
UnsolictedFiles.RemoveAt(0, NumUnsolictedFiles);
|
|
|
|
|
|
UnsolicitedFilesTime += 1000.0f * float(FPlatformTime::Seconds() - StartTime);
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogFileServer, Verbose, TEXT("Done Processing payload with Cmd %d Total Size sending %d "), Cmd,Out.TotalSize());
|
|
|
|
return Result;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessOpenFile( FArchive& In, FArchive& Out, bool bIsWriting )
|
|
{
|
|
// Get filename
|
|
FString Filename;
|
|
In << Filename;
|
|
|
|
bool bAppend = false;
|
|
bool bAllowRead = false;
|
|
|
|
if (bIsWriting)
|
|
{
|
|
In << bAppend;
|
|
In << bAllowRead;
|
|
}
|
|
|
|
// todo: clients from the same ip address "could" be trying to write to the same file in the same sandbox (for example multiple windows clients)
|
|
// should probably have the sandbox write to separate files for each client
|
|
// not important for now
|
|
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
if (bIsWriting)
|
|
{
|
|
// Make sure the directory exists...
|
|
Sandbox->CreateDirectoryTree(*(FPaths::GetPath(Filename)));
|
|
}
|
|
|
|
TArray<FString> NewUnsolictedFiles;
|
|
NetworkFileDelegates->FileRequestDelegate.ExecuteIfBound(Filename, ConnectedPlatformName, NewUnsolictedFiles);
|
|
|
|
// Disable access to outside the sandbox to prevent sending uncooked packages to the client
|
|
const bool bSandboxOnly = bRestrictPackageAssetsToSandbox && !bIsWriting && FPackageName::IsPackageExtension(*FPaths::GetExtension(Filename, true));
|
|
FSandboxOnlyScope _(*Sandbox, bSandboxOnly);
|
|
|
|
FDateTime ServerTimeStamp = Sandbox->GetTimeStamp(*Filename);
|
|
int64 ServerFileSize = 0;
|
|
IFileHandle* File = bIsWriting ? Sandbox->OpenWrite(*Filename, bAppend, bAllowRead) : Sandbox->OpenRead(*Filename);
|
|
if (!File)
|
|
{
|
|
UE_LOG(LogFileServer, Display, TEXT("Open request for %s failed for file %s."), bIsWriting ? TEXT("Writing") : TEXT("Reading"), *Filename);
|
|
ServerTimeStamp = FDateTime::MinValue(); // if this was a directory, this will make sure it is not confused with a zero byte file
|
|
}
|
|
else
|
|
{
|
|
ServerFileSize = File->Size();
|
|
}
|
|
|
|
uint64 HandleId = ++LastHandleId;
|
|
OpenFiles.Add( HandleId, File );
|
|
|
|
Out << HandleId;
|
|
Out << ServerTimeStamp;
|
|
Out << ServerFileSize;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessReadFile( FArchive& In, FArchive& Out )
|
|
{
|
|
// Get Handle ID
|
|
uint64 HandleId = 0;
|
|
In << HandleId;
|
|
|
|
int64 BytesToRead = 0;
|
|
In << BytesToRead;
|
|
|
|
int64 BytesRead = 0;
|
|
IFileHandle* File = FindOpenFile(HandleId);
|
|
|
|
if (File)
|
|
{
|
|
uint8* Dest = (uint8*)FMemory::Malloc(BytesToRead);
|
|
|
|
if (File->Read(Dest, BytesToRead))
|
|
{
|
|
BytesRead = BytesToRead;
|
|
Out << BytesRead;
|
|
Out.Serialize(Dest, BytesRead);
|
|
}
|
|
else
|
|
{
|
|
Out << BytesRead;
|
|
}
|
|
|
|
FMemory::Free(Dest);
|
|
}
|
|
else
|
|
{
|
|
Out << BytesRead;
|
|
}
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessWriteFile( FArchive& In, FArchive& Out )
|
|
{
|
|
// Get Handle ID
|
|
uint64 HandleId = 0;
|
|
In << HandleId;
|
|
|
|
int64 BytesWritten = 0;
|
|
IFileHandle* File = FindOpenFile(HandleId);
|
|
|
|
if (File)
|
|
{
|
|
int64 BytesToWrite = 0;
|
|
In << BytesToWrite;
|
|
|
|
uint8* Source = (uint8*)FMemory::Malloc(BytesToWrite);
|
|
In.Serialize(Source, BytesToWrite);
|
|
|
|
if (File->Write(Source, BytesToWrite))
|
|
{
|
|
BytesWritten = BytesToWrite;
|
|
}
|
|
|
|
FMemory::Free(Source);
|
|
}
|
|
|
|
Out << BytesWritten;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessSeekFile( FArchive& In, FArchive& Out )
|
|
{
|
|
// Get Handle ID
|
|
uint64 HandleId = 0;
|
|
In << HandleId;
|
|
|
|
int64 NewPosition;
|
|
In << NewPosition;
|
|
|
|
int64 SetPosition = -1;
|
|
IFileHandle* File = FindOpenFile(HandleId);
|
|
|
|
if (File && File->Seek(NewPosition))
|
|
{
|
|
SetPosition = File->Tell();
|
|
}
|
|
|
|
Out << SetPosition;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessCloseFile( FArchive& In, FArchive& Out )
|
|
{
|
|
// Get Handle ID
|
|
uint64 HandleId = 0;
|
|
In << HandleId;
|
|
|
|
uint32 Closed = 0;
|
|
IFileHandle* File = FindOpenFile(HandleId);
|
|
|
|
if (File)
|
|
{
|
|
Closed = 1;
|
|
OpenFiles.Remove(HandleId);
|
|
|
|
delete File;
|
|
}
|
|
|
|
Out << Closed;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessGetFileInfo( FArchive& In, FArchive& Out )
|
|
{
|
|
// Get filename
|
|
FString Filename;
|
|
In << Filename;
|
|
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
FFileInfo Info;
|
|
Info.FileExists = Sandbox->FileExists(*Filename);
|
|
|
|
// if the file exists, cook it if necessary (the FileExists flag won't change value based on this callback)
|
|
// without this, the server can return the uncooked file size, which can cause reads off the end
|
|
if (Info.FileExists)
|
|
{
|
|
TArray<FString> NewUnsolictedFiles;
|
|
NetworkFileDelegates->FileRequestDelegate.ExecuteIfBound(Filename, ConnectedPlatformName, NewUnsolictedFiles);
|
|
}
|
|
|
|
// get the rest of the info
|
|
Info.ReadOnly = Sandbox->IsReadOnly(*Filename);
|
|
Info.Size = Sandbox->FileSize(*Filename);
|
|
Info.TimeStamp = Sandbox->GetTimeStamp(*Filename);
|
|
Info.AccessTimeStamp = Sandbox->GetAccessTimeStamp(*Filename);
|
|
|
|
Out << Info.FileExists;
|
|
Out << Info.ReadOnly;
|
|
Out << Info.Size;
|
|
Out << Info.TimeStamp;
|
|
Out << Info.AccessTimeStamp;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessMoveFile( FArchive& In, FArchive& Out )
|
|
{
|
|
FString From;
|
|
In << From;
|
|
FString To;
|
|
In << To;
|
|
|
|
ConvertClientFilenameToServerFilename(From);
|
|
ConvertClientFilenameToServerFilename(To);
|
|
|
|
uint32 Success = Sandbox->MoveFile(*To, *From);
|
|
Out << Success;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessDeleteFile( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Filename;
|
|
In << Filename;
|
|
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
uint32 Success = Sandbox->DeleteFile(*Filename);
|
|
Out << Success;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessReportLocalFiles( FArchive& In, FArchive& Out )
|
|
{
|
|
// get the list of files on the other end
|
|
TMap<FString, FDateTime> ClientFileTimes;
|
|
In << ClientFileTimes;
|
|
|
|
// go over them and compare times to this side
|
|
TArray<FString> OutOfDateFiles;
|
|
|
|
for (TMap<FString, FDateTime>::TIterator It(ClientFileTimes); It; ++It)
|
|
{
|
|
FString ClientFile = It.Key();
|
|
ConvertClientFilenameToServerFilename(ClientFile);
|
|
// get the local timestamp
|
|
FDateTime Timestamp = Sandbox->GetTimeStamp(*ClientFile);
|
|
|
|
// if it's newer than the client/remote timestamp, it's newer here, so tell the other side it's out of date
|
|
if (Timestamp > It.Value())
|
|
{
|
|
OutOfDateFiles.Add(ClientFile);
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogFileServer, Display, TEXT("There were %d out of date files"), OutOfDateFiles.Num());
|
|
}
|
|
|
|
|
|
/** Copies file. */
|
|
void FNetworkFileServerClientConnection::ProcessCopyFile( FArchive& In, FArchive& Out )
|
|
{
|
|
FString To;
|
|
FString From;
|
|
In << To;
|
|
In << From;
|
|
|
|
ConvertClientFilenameToServerFilename(To);
|
|
ConvertClientFilenameToServerFilename(From);
|
|
|
|
bool Success = Sandbox->CopyFile(*To, *From);
|
|
Out << Success;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessSetTimeStamp( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Filename;
|
|
FDateTime Timestamp;
|
|
In << Filename;
|
|
In << Timestamp;
|
|
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
Sandbox->SetTimeStamp(*Filename, Timestamp);
|
|
|
|
// Need to sends something back otherwise the response won't get sent at all.
|
|
bool Success = true;
|
|
Out << Success;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessSetReadOnly( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Filename;
|
|
bool bReadOnly;
|
|
In << Filename;
|
|
In << bReadOnly;
|
|
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
bool Success = Sandbox->SetReadOnly(*Filename, bReadOnly);
|
|
Out << Success;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessCreateDirectory( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Directory;
|
|
In << Directory;
|
|
|
|
ConvertClientFilenameToServerFilename(Directory);
|
|
|
|
bool bSuccess = Sandbox->CreateDirectory(*Directory);
|
|
Out << bSuccess;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessDeleteDirectory( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Directory;
|
|
In << Directory;
|
|
|
|
ConvertClientFilenameToServerFilename(Directory);
|
|
|
|
bool bSuccess = Sandbox->DeleteDirectory(*Directory);
|
|
Out << bSuccess;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessDeleteDirectoryRecursively( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Directory;
|
|
In << Directory;
|
|
|
|
ConvertClientFilenameToServerFilename(Directory);
|
|
|
|
bool bSuccess = Sandbox->DeleteDirectoryRecursively(*Directory);
|
|
Out << bSuccess;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessToAbsolutePathForRead( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Filename;
|
|
In << Filename;
|
|
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
Filename = Sandbox->ConvertToAbsolutePathForExternalAppForRead(*Filename);
|
|
Out << Filename;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessToAbsolutePathForWrite( FArchive& In, FArchive& Out )
|
|
{
|
|
FString Filename;
|
|
In << Filename;
|
|
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
Filename = Sandbox->ConvertToAbsolutePathForExternalAppForWrite(*Filename);
|
|
Out << Filename;
|
|
}
|
|
|
|
static void AddDirectoriesToIgnore(const FString& RootDir, TArray<FString>& OutDirectoriesToSkip, TArray<FString>& OutDirectoriesToNotRecurse)
|
|
{
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Intermediate")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Documentation")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Extras")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Binaries")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Source")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Saved")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Plugins")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Programs")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Platforms")));
|
|
OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Build")));
|
|
OutDirectoriesToNotRecurse.Add(FString(RootDir / TEXT("DerivedDataCache")));
|
|
}
|
|
|
|
static void ScanExtensionRootDirectory(FSandboxPlatformFile* Sandbox, const FString& RootDir, const TArray<FString>& RootDirectories, TMap<FString, FDateTime>& OutFileTimes)
|
|
{
|
|
// Ensure that the path from the extension root to any containing root directory is in the FileTimes map
|
|
for (int32 DirIndex = 0; DirIndex < RootDirectories.Num(); DirIndex++)
|
|
{
|
|
if (FPaths::IsUnderDirectory(RootDir, RootDirectories[DirIndex]))
|
|
{
|
|
FString PathUpwardSegment = RootDir;
|
|
do
|
|
{
|
|
OutFileTimes.Add(PathUpwardSegment, 0);
|
|
PathUpwardSegment = FPaths::GetPath(PathUpwardSegment);
|
|
} while (FPaths::IsUnderDirectory(PathUpwardSegment, RootDirectories[DirIndex]));
|
|
break;
|
|
}
|
|
}
|
|
|
|
TArray<FString> ExtensionDirectoriesToSkip;
|
|
TArray<FString> ExtensionDirectoriesToNotRecurse;
|
|
AddDirectoriesToIgnore(RootDir, ExtensionDirectoriesToSkip, ExtensionDirectoriesToNotRecurse);
|
|
FLocalTimestampDirectoryVisitor ExtensionVisitor(*Sandbox, ExtensionDirectoriesToSkip, ExtensionDirectoriesToNotRecurse, true);
|
|
Sandbox->IterateDirectory(*RootDir, ExtensionVisitor);
|
|
OutFileTimes.Append(ExtensionVisitor.FileTimes);
|
|
}
|
|
|
|
bool FNetworkFileServerClientConnection::ProcessGetFileList( FArchive& In, FArchive& Out )
|
|
{
|
|
// get the list of directories to process
|
|
TArray<FString> TargetPlatformNames;
|
|
FString GameName;
|
|
FString EngineRelativePath;
|
|
FString GameRelativePath;
|
|
FString EnginePlatformExtensionsRelativePath;
|
|
FString ProjectPlatformExtensionsRelativePath;
|
|
FString EnginePluginsRelativePath;
|
|
FString ProjectPluginsRelativePath;
|
|
TArray<FString> RootDirectories;
|
|
TMap<FString,FString> CustomPlatformData;
|
|
|
|
|
|
EConnectionFlags ConnectionFlags;
|
|
|
|
FString ClientVersionInfo;
|
|
FString TargetAddress;
|
|
|
|
In << TargetPlatformNames;
|
|
In << GameName;
|
|
In << EngineRelativePath;
|
|
In << GameRelativePath;
|
|
In << EnginePlatformExtensionsRelativePath;
|
|
In << ProjectPlatformExtensionsRelativePath;
|
|
In << EnginePluginsRelativePath;
|
|
In << ProjectPluginsRelativePath;
|
|
In << RootDirectories;
|
|
In << ConnectionFlags;
|
|
In << ClientVersionInfo;
|
|
In << TargetAddress;
|
|
In << CustomPlatformData;
|
|
|
|
if ( NetworkFileDelegates->NewConnectionDelegate.IsBound() )
|
|
{
|
|
bool bIsValidVersion = true;
|
|
for ( const FString& TargetPlatform : TargetPlatformNames )
|
|
{
|
|
bIsValidVersion &= NetworkFileDelegates->NewConnectionDelegate.Execute(ClientVersionInfo, TargetPlatform );
|
|
}
|
|
if ( bIsValidVersion == false )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const bool bIsStreamingRequest = (ConnectionFlags & EConnectionFlags::Streaming) == EConnectionFlags::Streaming;
|
|
|
|
ConnectedPlatformName = TEXT("");
|
|
ConnectedTargetPlatform = nullptr;
|
|
ConnectedIPAddress = TEXT("");
|
|
ConnectedTargetCustomData.Reset();
|
|
|
|
|
|
// if we didn't find one (and this is a dumb server - no active platforms), then just use what was sent
|
|
if (ActiveTargetPlatforms.Num() == 0)
|
|
{
|
|
ConnectedPlatformName = TargetPlatformNames[0];
|
|
}
|
|
// we only need to care about validating the connected platform if there are active targetplatforms
|
|
else
|
|
{
|
|
// figure out the best matching target platform for the set of valid ones
|
|
for (int32 TPIndex = 0; TPIndex < TargetPlatformNames.Num() && ConnectedPlatformName == TEXT(""); TPIndex++)
|
|
{
|
|
UE_LOG(LogFileServer, Display, TEXT(" Possible Target Platform from client: %s"), *TargetPlatformNames[TPIndex]);
|
|
|
|
// look for a matching target platform
|
|
for (int32 ActiveTPIndex = 0; ActiveTPIndex < ActiveTargetPlatforms.Num(); ActiveTPIndex++)
|
|
{
|
|
UE_LOG(LogFileServer, Display, TEXT(" Checking against: %s"), *ActiveTargetPlatforms[ActiveTPIndex]->PlatformName());
|
|
if (ActiveTargetPlatforms[ActiveTPIndex]->PlatformName() == TargetPlatformNames[TPIndex])
|
|
{
|
|
bSendLowerCase = ActiveTargetPlatforms[ActiveTPIndex]->SendLowerCaseFilePaths();
|
|
ConnectedPlatformName = ActiveTargetPlatforms[ActiveTPIndex]->PlatformName();
|
|
ConnectedTargetPlatform = ActiveTargetPlatforms[ActiveTPIndex];
|
|
ConnectedIPAddress = TargetAddress;
|
|
ConnectedTargetCustomData = MoveTemp(CustomPlatformData);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we didn't find one, reject client and also print some warnings
|
|
if (ConnectedPlatformName == TEXT(""))
|
|
{
|
|
// reject client we can't cook/compile shaders for you!
|
|
UE_LOG(LogFileServer, Warning, TEXT("Unable to find target platform for client, terminating client connection!"));
|
|
|
|
for (int32 TPIndex = 0; TPIndex < TargetPlatformNames.Num() && ConnectedPlatformName == TEXT(""); TPIndex++)
|
|
{
|
|
UE_LOG(LogFileServer, Warning, TEXT(" Target platforms from client: %s"), *TargetPlatformNames[TPIndex]);
|
|
}
|
|
for (int32 ActiveTPIndex = 0; ActiveTPIndex < ActiveTargetPlatforms.Num(); ActiveTPIndex++)
|
|
{
|
|
UE_LOG(LogFileServer, Warning, TEXT(" Active target platforms on server: %s"), *ActiveTargetPlatforms[ActiveTPIndex]->PlatformName());
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ConnectedEngineDir = EngineRelativePath;
|
|
ConnectedProjectDir = GameRelativePath;
|
|
ConnectedEnginePlatformExtensionsDir = EnginePlatformExtensionsRelativePath;
|
|
ConnectedProjectPlatformExtensionsDir = ProjectPlatformExtensionsRelativePath;
|
|
|
|
UE_LOG(LogFileServer, Display, TEXT(" Connected EngineDir = %s"), *ConnectedEngineDir);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Local EngineDir = %s"), *LocalEngineDir);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Connected ProjectDir = %s"), *ConnectedProjectDir);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Local ProjectDir = %s"), *LocalProjectDir);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Connected EnginePlatformExtDir = %s"), *ConnectedEnginePlatformExtensionsDir);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Local EnginePlatformExtDir = %s"), *LocalEnginePlatformExtensionsDir);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Connected ProjectPlatformExtDir = %s"), *ConnectedProjectPlatformExtensionsDir);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Local ProjectPlatformExtDir = %s"), *LocalProjectPlatformExtensionsDir);
|
|
|
|
// Remap the root directories requested...
|
|
for (int32 RootDirIdx = 0; RootDirIdx < RootDirectories.Num(); RootDirIdx++)
|
|
{
|
|
FString CheckRootDir = RootDirectories[RootDirIdx];
|
|
ConvertClientFilenameToServerFilename(CheckRootDir);
|
|
RootDirectories[RootDirIdx] = CheckRootDir;
|
|
}
|
|
|
|
// figure out the sandbox directory
|
|
// @todo: This should use FPlatformMisc::SavedDirectory(GameName)
|
|
FString SandboxDirectory;
|
|
if (NetworkFileDelegates->SandboxPathOverrideDelegate.IsBound() )
|
|
{
|
|
SandboxDirectory = NetworkFileDelegates->SandboxPathOverrideDelegate.Execute();
|
|
// if the sandbox directory delegate returns a path with the platform name in it then replace it :)
|
|
SandboxDirectory.ReplaceInline(TEXT("[Platform]"), *ConnectedPlatformName);
|
|
}
|
|
else if ( FPaths::IsProjectFilePathSet() )
|
|
{
|
|
FString ProjectDir = FPaths::GetPath(FPaths::GetProjectFilePath());
|
|
SandboxDirectory = FPaths::Combine(*ProjectDir, TEXT("Saved"), TEXT("Cooked"), *ConnectedPlatformName);
|
|
|
|
// this is a workaround because the cooker and the networkfile server don't have access to eachother and therefore don't share the same Sandbox
|
|
// the cooker in cook in editor saves to the EditorCooked directory
|
|
if ( GIsEditor && !IsRunningCommandlet())
|
|
{
|
|
SandboxDirectory = FPaths::Combine(*ProjectDir, TEXT("Saved"), TEXT("EditorCooked"), *ConnectedPlatformName);
|
|
}
|
|
if( bIsStreamingRequest )
|
|
{
|
|
RootDirectories.Add(ProjectDir);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (FPaths::GetExtension(GameName) == FProjectDescriptor::GetExtension())
|
|
{
|
|
SandboxDirectory = FPaths::Combine(*FPaths::GetPath(GameName), TEXT("Saved"), TEXT("Cooked"), *ConnectedPlatformName);
|
|
}
|
|
else
|
|
{
|
|
//@todo: This assumes the game is located in the Unreal Root directory
|
|
SandboxDirectory = FPaths::Combine(*FPaths::GetRelativePathToRoot(), *GameName, TEXT("Saved"), TEXT("Cooked"), *ConnectedPlatformName);
|
|
}
|
|
}
|
|
// Convert to full path so that the sandbox wrapper doesn't re-base to Saved/Sandboxes
|
|
SandboxDirectory = FPaths::ConvertRelativePathToFull(SandboxDirectory);
|
|
|
|
// delete any existing one first, in case game name somehow changed and client is re-asking for files (highly unlikely)
|
|
Sandbox.Release();
|
|
Sandbox = FSandboxPlatformFile::Create(false);
|
|
Sandbox->Initialize(&FPlatformFileManager::Get().GetPlatformFile(), *FString::Printf(TEXT("-sandbox=\"%s\""), *SandboxDirectory));
|
|
|
|
GetSandboxRootDirectories(Sandbox.Get(), SandboxEngine, SandboxProject, SandboxEnginePlatformExtensions, SandboxProjectPlatformExtensions, LocalEngineDir, LocalProjectDir, LocalEnginePlatformExtensionsDir, LocalProjectPlatformExtensionsDir);
|
|
|
|
|
|
// make sure the global shaders are up to date before letting the client read any shaders
|
|
// @todo: This will probably add about 1/2 second to the boot-up time of the client while the server does this
|
|
// @note: We assume the delegate will write to the proper sandbox directory, should we pass in SandboxDirectory, or Sandbox?
|
|
FShaderRecompileData RecompileData;
|
|
RecompileData.PlatformName = ConnectedPlatformName;
|
|
// All target platforms
|
|
RecompileData.ShaderPlatform = -1;
|
|
RecompileData.ModifiedFiles = NULL;
|
|
RecompileData.MeshMaterialMaps = NULL;
|
|
NetworkFileDelegates->RecompileShadersDelegate.ExecuteIfBound(RecompileData);
|
|
|
|
UE_LOG(LogFileServer, Display, TEXT("Getting files for %d directories, game = %s, platform = %s"), RootDirectories.Num(), *GameName, *ConnectedPlatformName);
|
|
UE_LOG(LogFileServer, Display, TEXT(" Sandbox dir = %s"), *SandboxDirectory);
|
|
|
|
for (int32 DumpIdx = 0; DumpIdx < RootDirectories.Num(); DumpIdx++)
|
|
{
|
|
UE_LOG(LogFileServer, Display, TEXT("\t%s"), *(RootDirectories[DumpIdx]));
|
|
}
|
|
|
|
TArray<FString> DirectoriesToAlwaysStageAsUFS;
|
|
if ( GConfig->GetArray(TEXT("/Script/UnrealEd.ProjectPackagingSettings"), TEXT("DirectoriesToAlwaysStageAsUFS"), DirectoriesToAlwaysStageAsUFS, GGameIni) )
|
|
{
|
|
for ( const auto& DirectoryToAlwaysStage : DirectoriesToAlwaysStageAsUFS )
|
|
{
|
|
RootDirectories.Add( DirectoryToAlwaysStage );
|
|
}
|
|
}
|
|
|
|
// list of directories to skip
|
|
TArray<FString> DirectoriesToSkip;
|
|
TArray<FString> DirectoriesToNotRecurse;
|
|
// @todo: This should really be FPlatformMisc::GetSavedDirForGame(ClientGameName), etc
|
|
for (const FString& RootDir : RootDirectories)
|
|
{
|
|
AddDirectoriesToIgnore(RootDir, DirectoriesToSkip, DirectoriesToNotRecurse);
|
|
}
|
|
|
|
UE_LOG(LogFileServer, Display, TEXT("Scanning server files for timestamps..."));
|
|
|
|
double FileScanStartTime = FPlatformTime::Seconds();
|
|
|
|
// use the timestamp grabbing visitor (include directories)
|
|
FLocalTimestampDirectoryVisitor Visitor(*Sandbox, DirectoriesToSkip, DirectoriesToNotRecurse, true);
|
|
for (int32 DirIndex = 0; DirIndex < RootDirectories.Num(); DirIndex++)
|
|
{
|
|
bool bIsSubDirOfOtherRootDir = false;
|
|
for (int32 OtherDirIndex = 0; OtherDirIndex < DirIndex; OtherDirIndex++)
|
|
{
|
|
if (OtherDirIndex == DirIndex)
|
|
continue;
|
|
|
|
if (FPaths::IsUnderDirectory(RootDirectories[DirIndex], RootDirectories[OtherDirIndex]))
|
|
{
|
|
bIsSubDirOfOtherRootDir = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bIsSubDirOfOtherRootDir)
|
|
Sandbox->IterateDirectory(*RootDirectories[DirIndex], Visitor);
|
|
}
|
|
|
|
// Traverse plugin directories
|
|
TArray<TSharedRef<IPlugin>> AllPlugins = IPluginManager::Get().GetDiscoveredPlugins();
|
|
for (TSharedRef<IPlugin> Plugin : AllPlugins)
|
|
{
|
|
ScanExtensionRootDirectory(Sandbox.Get(), Plugin->GetBaseDir(), RootDirectories, Visitor.FileTimes);
|
|
}
|
|
|
|
// Traverse platform extension directories
|
|
FString ServerEnginePlatformExtensionsRelativePath = EnginePlatformExtensionsRelativePath;
|
|
ConvertClientFilenameToServerFilename(ServerEnginePlatformExtensionsRelativePath);
|
|
FString ServerProjectPlatformExtensionsRelativePath = ProjectPlatformExtensionsRelativePath;
|
|
ConvertClientFilenameToServerFilename(ServerProjectPlatformExtensionsRelativePath);
|
|
for (const FString& TargetPlatform : TargetPlatformNames)
|
|
{
|
|
ScanExtensionRootDirectory(Sandbox.Get(), ServerEnginePlatformExtensionsRelativePath / TargetPlatform, RootDirectories, Visitor.FileTimes);
|
|
ScanExtensionRootDirectory(Sandbox.Get(), ServerProjectPlatformExtensionsRelativePath / TargetPlatform, RootDirectories, Visitor.FileTimes);
|
|
}
|
|
|
|
UE_LOG(LogFileServer, Display, TEXT("Scanned server files, found %d files in %.2f seconds"), Visitor.FileTimes.Num(), FPlatformTime::Seconds() - FileScanStartTime);
|
|
|
|
// report the package version information
|
|
// The downside of this is that ALL cooked data will get tossed on package version changes
|
|
FPackageFileVersion PackageFileUnrealVersion = GPackageFileUEVersion;
|
|
Out << PackageFileUnrealVersion;
|
|
int32 PackageFileLicenseeUnrealVersion = GPackageFileLicenseeUEVersion;
|
|
Out << PackageFileLicenseeUnrealVersion;
|
|
|
|
// Send *our* engine and game dirs
|
|
Out << LocalEngineDir;
|
|
Out << LocalProjectDir;
|
|
Out << LocalEnginePlatformExtensionsDir;
|
|
Out << LocalProjectPlatformExtensionsDir;
|
|
|
|
// return the files and their timestamps
|
|
TMap<FString, FDateTime> FixedTimes = FixupSandboxPathsForClient(Visitor.FileTimes);
|
|
Out << FixedTimes;
|
|
|
|
#if 0 // dump the list of files
|
|
for ( const auto& FileTime : Visitor.FileTimes)
|
|
{
|
|
UE_LOG(LogFileServer, Display, TEXT("Server list of files %s time %d"), *FileTime.Key, *FileTime.Value.ToString() );
|
|
}
|
|
#endif
|
|
// Do it again, preventing access to non-cooked files
|
|
if( bIsStreamingRequest == false )
|
|
{
|
|
TArray<FString> RootContentPaths;
|
|
FPackageName::QueryRootContentPaths(RootContentPaths);
|
|
TArray<FString> ContentFolders;
|
|
for (const auto& RootPath : RootContentPaths)
|
|
{
|
|
const FString& ContentFolder = FPackageName::LongPackageNameToFilename(RootPath);
|
|
|
|
FString ConnectedContentFolder = ContentFolder;
|
|
ConnectedContentFolder.ReplaceInline(*LocalEngineDir, *ConnectedEngineDir);
|
|
ConnectedContentFolder.ReplaceInline(*LocalEnginePlatformExtensionsDir, *ConnectedEnginePlatformExtensionsDir);
|
|
ConnectedContentFolder.ReplaceInline(*LocalProjectPlatformExtensionsDir, *ConnectedProjectPlatformExtensionsDir);
|
|
|
|
int32 ReplaceCount = 0;
|
|
|
|
// If one path is relative and the other isn't, convert both to absolute paths before trying to replace
|
|
if (FPaths::IsRelative(LocalProjectDir) != FPaths::IsRelative(ConnectedContentFolder))
|
|
{
|
|
FString AbsoluteLocalGameDir = FPaths::ConvertRelativePathToFull(LocalProjectDir);
|
|
FString AbsoluteConnectedContentFolder = FPaths::ConvertRelativePathToFull(ConnectedContentFolder);
|
|
ReplaceCount = AbsoluteConnectedContentFolder.ReplaceInline(*AbsoluteLocalGameDir, *ConnectedProjectDir);
|
|
if (ReplaceCount > 0)
|
|
{
|
|
ConnectedContentFolder = AbsoluteConnectedContentFolder;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReplaceCount = ConnectedContentFolder.ReplaceInline(*LocalProjectDir, *ConnectedProjectDir);
|
|
}
|
|
|
|
if (ReplaceCount == 0)
|
|
{
|
|
int32 GameDirOffset = ConnectedContentFolder.Find(ConnectedProjectDir, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
|
|
if (GameDirOffset != INDEX_NONE)
|
|
{
|
|
ConnectedContentFolder.RightChopInline(GameDirOffset, false);
|
|
}
|
|
}
|
|
|
|
ContentFolders.Add(ConnectedContentFolder);
|
|
}
|
|
Out << ContentFolders;
|
|
|
|
// return the cached files and their timestamps
|
|
// TODO: This second file list is now identical to the first. This should be cleaned up in the future to not send two lists.
|
|
Out << FixedTimes;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FNetworkFileServerClientConnection::FileModifiedCallback( const FString& Filename)
|
|
{
|
|
FScopeLock Lock(&ModifiedFilesSection);
|
|
|
|
// do we care about this file???
|
|
|
|
// translation here?
|
|
ModifiedFiles.AddUnique(Filename);
|
|
}
|
|
|
|
void FNetworkFileServerClientConnection::ProcessHeartbeat( FArchive& In, FArchive& Out )
|
|
{
|
|
TArray<FString> FixedupModifiedFiles;
|
|
// Protect the array
|
|
if (Sandbox)
|
|
{
|
|
FScopeLock Lock(&ModifiedFilesSection);
|
|
|
|
for (const auto& ModifiedFile : ModifiedFiles)
|
|
{
|
|
FixedupModifiedFiles.Add(FixupSandboxPathForClient(ModifiedFile));
|
|
}
|
|
ModifiedFiles.Empty();
|
|
}
|
|
// return the list of modified files
|
|
Out << FixedupModifiedFiles;
|
|
|
|
|
|
|
|
// @todo: note the last received time, and toss clients that don't heartbeat enough!
|
|
|
|
// @todo: Right now, there is no directory watcher adding to ModifiedFiles. It had to be pulled from this thread (well, the ModuleManager part)
|
|
// We should have a single directory watcher that pushes the changes to all the connections - or possibly pass in a shared DirectoryWatcher
|
|
// and have each connection set up a delegate (see p4 history for HandleDirectoryWatcherDirectoryChanged)
|
|
}
|
|
|
|
|
|
/* FStreamingNetworkFileServerConnection callbacks
|
|
*****************************************************************************/
|
|
|
|
bool FNetworkFileServerClientConnection::PackageFile( FString& Filename, FString& TargetFilename, FArchive& Out )
|
|
{
|
|
// get file timestamp and send it to client
|
|
FDateTime ServerTimeStamp = Sandbox->GetTimeStamp(*Filename);
|
|
|
|
// Disable access to outside the sandbox to prevent sending uncooked packages to the client
|
|
const bool bSandboxOnly = bRestrictPackageAssetsToSandbox && FPackageName::IsPackageExtension(*FPaths::GetExtension(Filename, true));
|
|
FSandboxOnlyScope _(*Sandbox, bSandboxOnly);
|
|
|
|
FString AbsHostFile = Sandbox->ConvertToAbsolutePathForExternalAppForRead(*Filename);
|
|
if (ConnectedTargetPlatform != nullptr && ConnectedTargetPlatform->CopyFileToTarget(ConnectedIPAddress, AbsHostFile, TargetFilename, ConnectedTargetCustomData))
|
|
{
|
|
Out << Filename;
|
|
Out << ServerTimeStamp;
|
|
// MAX_uint64 here indicates that it was copied already
|
|
uint64 FileSize = MAX_uint64;
|
|
Out << FileSize;
|
|
|
|
return true;
|
|
}
|
|
|
|
TArray<uint8> Contents;
|
|
// open file
|
|
IFileHandle* File = Sandbox->OpenRead(*Filename);
|
|
bool bRetVal = true;
|
|
|
|
if (!File)
|
|
{
|
|
++PackageRequestsFailed;
|
|
|
|
UE_LOG(LogFileServer, Warning, TEXT("Opening file %s failed"), *Filename);
|
|
ServerTimeStamp = FDateTime::MinValue(); // if this was a directory, this will make sure it is not confused with a zero byte file
|
|
bRetVal = false;
|
|
}
|
|
else
|
|
{
|
|
++PackageRequestsSucceeded;
|
|
|
|
if (!File->Size())
|
|
{
|
|
UE_LOG(LogFileServer, Warning, TEXT("Sending empty file %s...."), *Filename);
|
|
}
|
|
else
|
|
{
|
|
FileBytesSent += File->Size();
|
|
// read it
|
|
Contents.AddUninitialized(File->Size());
|
|
File->Read(Contents.GetData(), Contents.Num());
|
|
}
|
|
|
|
// close it
|
|
delete File;
|
|
|
|
UE_LOG(LogFileServer, Display, TEXT("Read %s, %d bytes"), *Filename, Contents.Num());
|
|
}
|
|
|
|
Out << Filename;
|
|
Out << ServerTimeStamp;
|
|
uint64 FileSize = Contents.Num();
|
|
Out << FileSize;
|
|
Out.Serialize(Contents.GetData(), FileSize);
|
|
return bRetVal;
|
|
}
|
|
|
|
|
|
void FNetworkFileServerClientConnection::ProcessRecompileShaders( FArchive& In, FArchive& Out )
|
|
{
|
|
TArray<FString> RecompileModifiedFiles;
|
|
TArray<uint8> MeshMaterialMaps;
|
|
FShaderRecompileData RecompileData;
|
|
RecompileData.PlatformName = ConnectedPlatformName;
|
|
RecompileData.ModifiedFiles = &RecompileModifiedFiles;
|
|
RecompileData.MeshMaterialMaps = &MeshMaterialMaps;
|
|
|
|
// tell other side all the materials to load, by pathname
|
|
In << RecompileData.MaterialsToLoad;
|
|
In << RecompileData.ShaderPlatform;
|
|
In << RecompileData.bCompileChangedShaders;
|
|
In << RecompileData.ShadersToRecompile;
|
|
|
|
NetworkFileDelegates->RecompileShadersDelegate.ExecuteIfBound(RecompileData);
|
|
|
|
// tell other side what to do!
|
|
Out << RecompileModifiedFiles;
|
|
Out << MeshMaterialMaps;
|
|
}
|
|
|
|
|
|
bool FNetworkFileServerClientConnection::ProcessSyncFile( FArchive& In, FArchive& Out )
|
|
{
|
|
|
|
double StartTime;
|
|
StartTime = FPlatformTime::Seconds();
|
|
|
|
// get filename
|
|
FString Filename;
|
|
In << Filename;
|
|
|
|
UE_LOG(LogFileServer, Verbose, TEXT("Try sync file %s"), *Filename);
|
|
|
|
FString ClientFilename = Filename;
|
|
ConvertClientFilenameToServerFilename(Filename);
|
|
|
|
//FString AbsFile(FString(*Sandbox->ConvertToAbsolutePathForExternalApp(*Filename)).MakeStandardFilename());
|
|
// ^^ we probably in general want that filename, but for cook on the fly, we want the un-sandboxed name
|
|
|
|
TArray<FString> NewUnsolictedFiles;
|
|
|
|
NetworkFileDelegates->FileRequestDelegate.ExecuteIfBound(Filename, ConnectedPlatformName, NewUnsolictedFiles);
|
|
|
|
FileRequestDelegateTime += 1000.0f * float(FPlatformTime::Seconds() - StartTime);
|
|
StartTime = FPlatformTime::Seconds();
|
|
|
|
|
|
for (int32 Index = 0; Index < NewUnsolictedFiles.Num(); Index++)
|
|
{
|
|
if (NewUnsolictedFiles[Index] != Filename)
|
|
{
|
|
UnsolictedFiles.AddUnique(NewUnsolictedFiles[Index]);
|
|
}
|
|
}
|
|
|
|
bool bRetVal = PackageFile(Filename, ClientFilename, Out);
|
|
|
|
PackageFileTime += 1000.0f * float(FPlatformTime::Seconds() - StartTime);
|
|
|
|
return bRetVal;
|
|
}
|
|
|
|
FString FNetworkFileServerClientConnection::GetDescription() const
|
|
{
|
|
return FString("Client For " ) + ConnectedPlatformName;
|
|
}
|
|
|
|
|
|
bool FNetworkFileServerClientConnection::Exec(class UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar )
|
|
{
|
|
if (FParse::Command(&Cmd, TEXT("networkserverconnection")))
|
|
{
|
|
if (FParse::Command(&Cmd, TEXT("stats")))
|
|
{
|
|
|
|
Ar.Logf(TEXT("Network server connection %s stats\n"
|
|
"FileRequestDelegateTime \t%fms \n"
|
|
"PackageFileTime \t%fms \n"
|
|
"UnsolicitedFilesTime \t%fms \n"
|
|
"FileRequestCount \t%d \n"
|
|
"UnsolicitedFilesCount \t%d \n"
|
|
"PackageRequestsSucceeded \t%d \n"
|
|
"PackageRequestsFailed \t%d \n"
|
|
"FileBytesSent \t%d \n"),
|
|
*GetDescription(),
|
|
FileRequestDelegateTime,
|
|
PackageFileTime,
|
|
UnsolicitedFilesTime,
|
|
FileRequestCount,
|
|
UnsolicitedFilesCount,
|
|
PackageRequestsSucceeded,
|
|
PackageRequestsFailed,
|
|
FileBytesSent);
|
|
|
|
// there could be multiple network platform files so let them all report their stats
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|