// Copyright Epic Games, Inc. All Rights Reserved. #include "NetworkFileServerConnection.h" #include "HAL/PlatformFileManager.h" #include "Misc/Paths.h" #include "Misc/PathViews.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" #include "PlatformInfo.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, EAllowShrinking::No); } return Out; } struct FSandboxOnlyScope { FSandboxOnlyScope(FSandboxPlatformFile& InSandbox, bool bInSandboxOnly) : Sandbox(InSandbox) { Sandbox.SetSandboxOnly(bInSandboxOnly); } ~FSandboxOnlyScope() { Sandbox.SetSandboxOnly(false); } FSandboxPlatformFile& Sandbox; }; // These are marked unsafe because they do not work with Programs. However, COTF is unlikely to be used with Programs // These are also temporary until some issues can be debugged static FString UnsafeEnginePlatformExtensionDir() { return FPaths::EnginePlatformExtensionDir(TEXT("")).TrimChar('/'); } static FString UnsafeProjectPlatformExtensionDir() { return FPaths::ProjectPlatformExtensionDir(TEXT("")).TrimChar('/'); } /* 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 = UnsafeEnginePlatformExtensionDir(); LocalProjectPlatformExtensionsDir = UnsafeProjectPlatformExtensionDir(); 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::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, UnsafeEnginePlatformExtensionDir(), ConnectedEnginePlatformExtensionsDir)) { return; } if (TrySubstituteDirectory(FilenameToConvert, UnsafeProjectPlatformExtensionDir(), 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 FNetworkFileServerClientConnection::FixupSandboxPathsForClient(const TMap& SandboxPaths) { TMap 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::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; 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 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 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 %" INT64_FMT " "), 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 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; IFileHandle* File = FindOpenFile(HandleId); if (File) { constexpr int64 BufferSize = 4 << 20; uint8* Buffer = (uint8*)FMemory::Malloc(BufferSize); bool bIsFirstRead = true; while (BytesToRead > 0) { int64 CappedBytesToRead = FMath::Min(BufferSize, BytesToRead); if (!File->Read(Buffer, CappedBytesToRead)) { if (bIsFirstRead) { int64 BytesRead = 0; Out << BytesRead; break; } // If this is not the first read we've already written the expected number of bytes to the stream so we have to deliver on that FMemory::Memset(Buffer, 0, CappedBytesToRead); } else if (bIsFirstRead) { Out << BytesToRead; } Out.Serialize(Buffer, CappedBytesToRead); BytesToRead -= CappedBytesToRead; bIsFirstRead = false; } FMemory::Free(Buffer); } else { int64 BytesRead = 0; 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; constexpr int64 BufferSize = 4 << 20; uint8* Buffer = (uint8*)FMemory::Malloc(BufferSize); while (BytesToWrite > 0) { int64 CappedBytesToWrite = FMath::Min(BufferSize, BytesToWrite); In.Serialize(Buffer, CappedBytesToWrite); if (!File->Write(Buffer, CappedBytesToWrite)) { break; } BytesWritten += CappedBytesToWrite; BytesToWrite -= CappedBytesToWrite; } FMemory::Free(Buffer); } 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 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 ClientFileTimes; In << ClientFileTimes; // go over them and compare times to this side TArray OutOfDateFiles; for (TMap::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& OutDirectoriesToSkip, TArray& 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"))); OutDirectoriesToSkip.Add(FString(RootDir / TEXT("Restricted"))); OutDirectoriesToNotRecurse.Add(FString(RootDir / TEXT("DerivedDataCache"))); } static void ScanExtensionRootDirectory(FSandboxPlatformFile* Sandbox, const FString& RootDir, const TArray& RootDirectories, TMap& 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 ExtensionDirectoriesToSkip; TArray 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 TargetPlatformNames; FString GameName; FString EngineRelativePath; FString GameRelativePath; FString EnginePlatformExtensionsRelativePath; FString ProjectPlatformExtensionsRelativePath; FString EnginePluginsRelativePath; FString ProjectPluginsRelativePath; TArray RootDirectories; TMap 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.Reset(); 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); 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 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 DirectoriesToSkip; TArray 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); } // Get PlatformDirectoryNames FString ServerEnginePlatformExtensionsRelativePath = EnginePlatformExtensionsRelativePath; ConvertClientFilenameToServerFilename(ServerEnginePlatformExtensionsRelativePath); FString ServerProjectPlatformExtensionsRelativePath = ProjectPlatformExtensionsRelativePath; ConvertClientFilenameToServerFilename(ServerProjectPlatformExtensionsRelativePath); TArray PlatformDirectoryNames; for (const FString& TargetPlatform : TargetPlatformNames) { FName IniPlatformName = PlatformInfo::FindPlatformInfo(*TargetPlatform)->IniPlatformName; const FDataDrivenPlatformInfo& PlatformInfo = FDataDrivenPlatformInfoRegistry::GetPlatformInfo(IniPlatformName); PlatformDirectoryNames.Reserve(PlatformInfo.IniParentChain.Num() + PlatformInfo.AdditionalRestrictedFolders.Num() + 1); PlatformDirectoryNames.Add(IniPlatformName.ToString()); for (const FString& PlatformName : PlatformInfo.AdditionalRestrictedFolders) { PlatformDirectoryNames.AddUnique(PlatformName); } for (const FString& PlatformName : PlatformInfo.IniParentChain) { PlatformDirectoryNames.AddUnique(PlatformName); } } // Traverse plugin directories TSet PlatformDirectoryNameSet; PlatformDirectoryNameSet.Append(PlatformDirectoryNames); TArray> AllPlugins = IPluginManager::Get().GetDiscoveredPlugins(); for (TSharedRef Plugin : AllPlugins) { // First the base directory of the plugin. ScanExtensionRootDirectory(Sandbox.Get(), Plugin->GetBaseDir(), RootDirectories, Visitor.FileTimes); // Next the plugin extension directories of this plugin. TArray PluginExtensionDirs = Plugin->GetExtensionBaseDirs(); for (const FString& ExtensionDir : PluginExtensionDirs) { // Scan for Platforms/X. If X is not one of our platforms do not scan this extension directory. // If X is one of our platforms or this extension is not Platforms restricted at all scan it. bool bFoundPlatforms = false; bool bDone = false; bool bWrongPlatform = false; FPathViews::IterateComponents( ExtensionDir, [&bFoundPlatforms, &bDone, &bWrongPlatform, &PlatformDirectoryNameSet](FStringView CurrentPathComponent) { if (!bFoundPlatforms) { if (CurrentPathComponent == FString(TEXT("Platforms"))) { bFoundPlatforms = true; } } else if (!bDone) { bWrongPlatform = !PlatformDirectoryNameSet.Contains(FString(CurrentPathComponent)); bDone = true; } else { // Do nothing. } } ); if (!bWrongPlatform) { ScanExtensionRootDirectory(Sandbox.Get(), ExtensionDir, RootDirectories, Visitor.FileTimes); } } } // Traverse platform extension directories for (const FString& PlatformDirectoryName : PlatformDirectoryNames) { ScanExtensionRootDirectory(Sandbox.Get(), ServerEnginePlatformExtensionsRelativePath / PlatformDirectoryName, RootDirectories, Visitor.FileTimes); ScanExtensionRootDirectory(Sandbox.Get(), ServerProjectPlatformExtensionsRelativePath / PlatformDirectoryName, 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 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 RootContentPaths; FPackageName::QueryRootContentPaths(RootContentPaths); TArray 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, EAllowShrinking::No); } } 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 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 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 { if (IntFitsIn(File->Size())) { int32 FileSize32 = static_cast(File->Size()); FileBytesSent += FileSize32; // read it Contents.AddUninitialized(FileSize32); File->Read(Contents.GetData(), Contents.Num()); } else { UE_LOG(LogFileServer, Warning, TEXT("Unable to open %s because it is too large"), *Filename); bRetVal = false; } } // 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; } 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 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_Runtime(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; }