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;
}
FArchive* FFileManagerGeneric::CreateFileWriter( const TCHAR* Filename, uint32 Flags )
{
	// Only allow writes to files that are not signed 
	// Except if the file is missing( that way corrupt ini files can be autogenerated by deleting them )
	if( FSHA1::GetFileSHAHash( Filename, NULL ) && FileSize( Filename ) != -1 )
	{
		UE_LOG( LogFileManager, Log, TEXT( "Can't write to signed game file: %s" ),Filename );
		return new FArchiveFileWriterDummy();
	}
	MakeDirectory( *FPaths::GetPath(Filename), true );

	if( Flags & FILEWRITE_EvenIfReadOnly )
	{
		GetLowLevel().SetReadOnly( Filename, false );
	}

	IFileHandle* Handle = GetLowLevel().OpenWrite( Filename, !!( Flags & FILEWRITE_Append ), !!( Flags & FILEWRITE_AllowRead ) );
	if( !Handle )
	{
		if( Flags & FILEWRITE_NoFail )
		{
			UE_LOG( LogFileManager, Fatal, TEXT( "Failed to create file: %s" ), Filename );
		}
		return NULL;
	}
	return new FArchiveFileWriterGeneric( Handle, Filename, Handle->Tell() );
}
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;
	}
}
/** 
 * Write out the steam app id to the steam_appid.txt file before initializing the API
 * @param SteamAppId id assigned to the application by Steam
 */
static void WriteSteamAppIdToDisk(int32 SteamAppId)
{
	if (SteamAppId > 0)
	{
		// Turn off sandbox temporarily to make sure file is where it's always expected
		FScopeSandboxContext ScopedSandbox(false);

		// Access the physical file writer directly so that we still write next to the executable in CotF builds.
		FString SteamAppIdFilename = GetSteamAppIdFilename();
		IFileHandle* Handle = IPlatformFile::GetPlatformPhysical().OpenWrite(*SteamAppIdFilename, false, false);
		if (!Handle)
		{
			UE_LOG_ONLINE(Fatal, TEXT("Failed to create file: %s"), *SteamAppIdFilename);
		}
		else
		{
			FString AppId = FString::Printf(TEXT("%d"), SteamAppId);

			FBufferArchive Archive;
			Archive.Serialize((void*)TCHAR_TO_ANSI(*AppId), AppId.Len());

			Handle->Write(Archive.GetData(), Archive.Num());
			delete Handle;
			Handle = nullptr;
		}
	}
}
bool FStreamingNetworkPlatformFile::InitializeInternal(IPlatformFile* Inner, const TCHAR* HostIP)
{
	// look for the commandline that will read files from over the network
	if (HostIP == nullptr)
	{
		UE_LOG(LogStreamingPlatformFile, Error, TEXT("No Host IP specified in the commandline."));
		bIsUsable = false;

		return false;
	}

	// optionally get the port from the command line
	int32 OverridePort;
	if (FParse::Value(FCommandLine::Get(), TEXT("fileserverport="), OverridePort))
	{
		UE_LOG(LogStreamingPlatformFile, Display, TEXT("Overriding file server port: %d"), OverridePort);
		FileServerPort = OverridePort;
	}

	// Send the filenames and timestamps to the server.
	FNetworkFileArchive Payload(NFS_Messages::GetFileList);
	FillGetFileList(Payload, true);

	// Send the directories over, and wait for a response.
	FArrayReader Response;

	if(SendPayloadAndReceiveResponse(Payload,Response))
	{
		// Receive the cooked version information.
		int32 ServerPackageVersion = 0;
		int32 ServerPackageLicenseeVersion = 0;
		ProcessServerInitialResponse(Response, ServerPackageVersion, ServerPackageLicenseeVersion);

		// Make sure we can sync a file.
		FString TestSyncFile = FPaths::Combine(*(FPaths::EngineDir()), TEXT("Config/BaseEngine.ini"));
		IFileHandle* TestFileHandle = OpenRead(*TestSyncFile);
		if (TestFileHandle != nullptr)
		{
			uint8* FileContents = (uint8*)FMemory::Malloc(TestFileHandle->Size());
			if (!TestFileHandle->Read(FileContents, TestFileHandle->Size()))
			{
				UE_LOG(LogStreamingPlatformFile, Fatal, TEXT("Could not read test file %s."), *TestSyncFile);
			}
			FMemory::Free(FileContents);
			delete TestFileHandle;
		}
		else
		{
			UE_LOG(LogStreamingPlatformFile, Fatal, TEXT("Could not open test file %s."), *TestSyncFile);
		}

		FCommandLine::AddToSubprocessCommandline( *FString::Printf( TEXT("-StreamingHostIP=%s"), HostIP ) );

		return true; 
	}

	return false; 
}
 bool CopyFileToDevice(const FString& IPAPath, FString PathOnDevice, int PacketSize)
 {
     bool Result = false;
     // reconnect to the device and start the AFC service
     Connect();
     if (!AFC::StartService(DeviceHandle, &AFCHandle))
     {
         if (!AFC::ConnectionOpen(AFCHandle, &AFCConnection))
         {
     
             // ensure the directory on the phone exists
             FString DirectoryOnDevice = FPaths::GetPath(PathOnDevice);
             CreateDirectory(DirectoryOnDevice + TEXT("/"));
     
             // transfer the file
             IPlatformFile& PlatformFile = IPlatformFile::GetPlatformPhysical();
             IFileHandle* SourceFile = PlatformFile.OpenRead(*IPAPath);
             if (SourceFile != NULL)
             {
                 uint64 DestinationHandle = 0;
                 if (AFC::FileRefOpen(AFCConnection, PathOnDevice, 3, &DestinationHandle) == 0)
                 {
                     int TotalBytes = 0;
                     int PacketCount = SourceFile->Size() / PacketSize;
                     uint8* buffer = new uint8[PacketSize];
                     for (int Index = 0; Index < PacketCount; ++Index)
                     {
                         if (SourceFile->Read(buffer, PacketSize))
                         {
                             TotalBytes += PacketSize;
                             AFC::FileRefWrite(AFCConnection, DestinationHandle, buffer, PacketSize);
                         }
                     }
             
                     if (SourceFile->Read(buffer, SourceFile->Size() - TotalBytes))
                     {
                         AFC::FileRefWrite(AFCConnection, DestinationHandle, buffer, SourceFile->Size() - TotalBytes);
                     }
             
                     // flush the destination and close
                     AFC::FileRefClose(AFCConnection, DestinationHandle);
                     
                     Result = true;
                 }
                 delete SourceFile;
             }
     
             // stop the AFC service and disconnect from the device
             AFC::ConnectionClose(AFCConnection);
             AFCConnection = NULL;
             Disconnect();
         }
     }
     return Result;
 }
FArchive* FFileManagerGeneric::CreateFileReader( const TCHAR* InFilename, uint32 Flags )
{
	IFileHandle* Handle = GetLowLevel().OpenRead( InFilename, !!(Flags & FILEREAD_AllowWrite) );
	if( !Handle )
	{
		if( Flags & FILEREAD_NoFail )
		{
			UE_LOG( LogFileManager, Fatal, TEXT( "Failed to read file: %s" ), InFilename );
		}
		return NULL;
	}
	return new FArchiveFileReaderGeneric( Handle, InFilename, Handle->Size() );
}
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;
	FileRequestDelegate.ExecuteIfBound(Filename, ConnectedPlatformName, NewUnsolictedFiles);

	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;
}
Esempio n. 9
0
/// <summary> Writes a full line at the end of the file created for this session </summary>
/// <param name="line"> The text line to append at the end of the file </param>
void UMovementTracker::WriteToCurrentFile(FString line)
{
	if (WritePosition && DirectoryExists)
	{
		IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
		IFileHandle* handle = PlatformFile.OpenWrite(*FileName, true);
		if (handle)
		{
			handle->Write((const uint8 *)TCHAR_TO_ANSI(*line), line.Len());
			handle->Write((const uint8 *)"\n", 1);
			delete handle;
		}
	}
}
bool FPakPlatformFile::BufferedCopyFile(IFileHandle& Dest, IFileHandle& Source, const int64 FileSize, uint8* Buffer, const int64 BufferSize) const
{	
	int64 RemainingSizeToCopy = FileSize;
	// Continue copying chunks using the buffer
	while (RemainingSizeToCopy > 0)
	{
		const int64 SizeToCopy = FMath::Min(BufferSize, RemainingSizeToCopy);
		if (Source.Read(Buffer, SizeToCopy) == false)
		{
			return false;
		}
		if (Dest.Write(Buffer, SizeToCopy) == false)
		{
			return false;
		}
		RemainingSizeToCopy -= SizeToCopy;
	}
	return true;
}
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::PackageFile( FString& Filename, FArchive& Out )
{
	// get file timestamp and send it to client
	FDateTime ServerTimeStamp = Sandbox->GetTimeStamp(*Filename);

	TArray<uint8> Contents;
	// open file
	IFileHandle* File = Sandbox->OpenRead(*Filename);

	if (!File)
	{
		ServerTimeStamp = FDateTime::MinValue(); // if this was a directory, this will make sure it is not confused with a zero byte file

		UE_LOG(LogFileServer, Warning, TEXT("Request for missing file %s."), *Filename );
	}
	else
	{
		if (!File->Size())
		{
			UE_LOG(LogFileServer, Warning, TEXT("Sending empty file %s...."), *Filename);
		}
		else
		{
			// 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);
}
void FNetworkPlatformFile::InitializeAfterSetActive()
{
	double NetworkFileStartupTime = 0.0;
	{
		SCOPE_SECONDS_COUNTER(NetworkFileStartupTime);

		// send the filenames and timestamps to the server
		FNetworkFileArchive Payload(NFS_Messages::GetFileList);
		FillGetFileList(Payload, false);

		// send the directories over, and wait for a response
		FArrayReader Response;
		if (!SendPayloadAndReceiveResponse(Payload, Response))
		{
			delete Transport; 
			return; 
		}
		else
		{
			// receive the cooked version information
			int32 ServerPackageVersion = 0;
			int32 ServerPackageLicenseeVersion = 0;
			ProcessServerInitialResponse(Response, ServerPackageVersion, ServerPackageLicenseeVersion);

			// receive a list of the cache files and their timestamps
			TMap<FString, FDateTime> ServerCachedFiles;
			Response << ServerCachedFiles;

			bool bDeleteAllFiles = true;
			// Check the stored cooked version
			FString CookedVersionFile = FPaths::GeneratedConfigDir() / TEXT("CookedVersion.txt");

			if (InnerPlatformFile->FileExists(*CookedVersionFile) == true)
			{
				IFileHandle* FileHandle = InnerPlatformFile->OpenRead(*CookedVersionFile);
				if (FileHandle != NULL)
				{
					int32 StoredPackageCookedVersion;
					int32 StoredPackageCookedLicenseeVersion;
					if (FileHandle->Read((uint8*)&StoredPackageCookedVersion, sizeof(int32)) == true)
					{
						if (FileHandle->Read((uint8*)&StoredPackageCookedLicenseeVersion, sizeof(int32)) == true)
						{
							if ((ServerPackageVersion == StoredPackageCookedVersion) &&
								(ServerPackageLicenseeVersion == StoredPackageCookedLicenseeVersion))
							{
								bDeleteAllFiles = false;
							}
							else
							{
								UE_LOG(LogNetworkPlatformFile, Display, 
									TEXT("Engine version mismatch: Server %d.%d, Stored %d.%d\n"), 
									ServerPackageVersion, ServerPackageLicenseeVersion,
									StoredPackageCookedVersion, StoredPackageCookedLicenseeVersion);
							}
						}
					}

					delete FileHandle;
				}
			}
			else
			{
				UE_LOG(LogNetworkPlatformFile, Display, TEXT("Cooked version file missing: %s\n"), *CookedVersionFile);
			}

			if (bDeleteAllFiles == true)
			{
				// Make sure the config file exists...
				InnerPlatformFile->CreateDirectoryTree(*(FPaths::GeneratedConfigDir()));
				// Update the cooked version file
				IFileHandle* FileHandle = InnerPlatformFile->OpenWrite(*CookedVersionFile);
				if (FileHandle != NULL)
				{
					FileHandle->Write((const uint8*)&ServerPackageVersion, sizeof(int32));
					FileHandle->Write((const uint8*)&ServerPackageLicenseeVersion, sizeof(int32));
					delete FileHandle;
				}
			}

			// list of directories to skip
			TArray<FString> DirectoriesToSkip;
			TArray<FString> DirectoriesToNotRecurse;
			// use the timestamp grabbing visitor to get all the content times
			FLocalTimestampDirectoryVisitor Visitor(*InnerPlatformFile, DirectoriesToSkip, DirectoriesToNotRecurse, false);

			TArray<FString> RootContentPaths;
			FPackageName::QueryRootContentPaths( RootContentPaths );
			for( TArray<FString>::TConstIterator RootPathIt( RootContentPaths ); RootPathIt; ++RootPathIt )
			{
				const FString& RootPath = *RootPathIt;
				const FString& ContentFolder = FPackageName::LongPackageNameToFilename(RootPath);

				InnerPlatformFile->IterateDirectory( *ContentFolder, Visitor);
			}

			// delete out of date files using the server cached files
			for (TMap<FString, FDateTime>::TIterator It(ServerCachedFiles); It; ++It)
			{
				bool bDeleteFile = bDeleteAllFiles;
				FString ServerFile = It.Key();

				// Convert the filename to the client version
				ConvertServerFilenameToClientFilename(ServerFile);

				// Set it in the visitor file times list
				Visitor.FileTimes.Add(ServerFile, FDateTime::MinValue());

				if (bDeleteFile == false)
				{
					// Check the time stamps...
					// get local time
					FDateTime LocalTime = InnerPlatformFile->GetTimeStamp(*ServerFile);
					// If local time == MinValue than the file does not exist in the cache.
					if (LocalTime != FDateTime::MinValue())
					{
						FDateTime ServerTime = It.Value();
						// delete if out of date
						// We will use 1.0 second as the tolerance to cover any platform differences in resolution
						FTimespan TimeDiff = LocalTime - ServerTime;
						double TimeDiffInSeconds = TimeDiff.GetTotalSeconds();
						bDeleteFile = (TimeDiffInSeconds > 1.0) || (TimeDiffInSeconds < -1.0);
						if (bDeleteFile == true)
						{
							if (InnerPlatformFile->FileExists(*ServerFile) == true)
							{
								UE_LOG(LogNetworkPlatformFile, Display, TEXT("Deleting cached file: TimeDiff %5.3f, %s"), TimeDiffInSeconds, *It.Key());
							}
							else
							{
								// It's a directory
								bDeleteFile = false;
							}
						}
					}
				}
				if (bDeleteFile == true)
				{
					InnerPlatformFile->DeleteFile(*ServerFile);
				}
			}

			// Any content files we have locally that were not cached, delete them
			for (TMap<FString, FDateTime>::TIterator It(Visitor.FileTimes); It; ++It)
			{
				if (It.Value() != FDateTime::MinValue())
				{
					// This was *not* found in the server file list... delete it
					UE_LOG(LogNetworkPlatformFile, Display, TEXT("Deleting cached file: %s"), *It.Key());
					InnerPlatformFile->DeleteFile(*It.Key());
				}
			}

			// make sure we can sync a file
			FString TestSyncFile = FPaths::Combine(*(FPaths::EngineDir()), TEXT("Config/BaseEngine.ini"));

			InnerPlatformFile->SetReadOnly(*TestSyncFile, false);
			InnerPlatformFile->DeleteFile(*TestSyncFile);
			if (InnerPlatformFile->FileExists(*TestSyncFile))
			{
				UE_LOG(LogNetworkPlatformFile, Fatal, TEXT("Could not delete file sync test file %s."), *TestSyncFile);
			}

			EnsureFileIsLocal(TestSyncFile);

			if (!InnerPlatformFile->FileExists(*TestSyncFile) || InnerPlatformFile->FileSize(*TestSyncFile) < 1)
			{
				UE_LOG(LogNetworkPlatformFile, Fatal, TEXT("Could not sync test file %s."), *TestSyncFile);
			}
		}
	}

	FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Network file startup time: %5.3f seconds\n"), NetworkFileStartupTime);

}
/**
 * Kicks directory watcher test/
 */
int32 DirectoryWatcherTest(const TCHAR* CommandLine)
{
	FPlatformMisc::SetCrashHandler(NULL);
	FPlatformMisc::SetGracefulTerminationHandler();

	GEngineLoop.PreInit(CommandLine);
	UE_LOG(LogTestPAL, Display, TEXT("Running directory watcher test."));

	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
	FString TestDir = FString::Printf(TEXT("%sDirectoryWatcherTest%d"), FPlatformProcess::UserTempDir(), FPlatformProcess::GetCurrentProcessId());

	if (PlatformFile.CreateDirectory(*TestDir) && PlatformFile.CreateDirectory(*(TestDir + TEXT("/subtest"))))
	{
		FChangeDetector Detector;
		FDelegateHandle DirectoryChangedHandle;

		IDirectoryWatcher* DirectoryWatcher = FModuleManager::Get().LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher")).Get();
		if (DirectoryWatcher)
		{
			auto Callback = IDirectoryWatcher::FDirectoryChanged::CreateRaw(&Detector, &FChangeDetector::OnDirectoryChanged);
			DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(TestDir, Callback, DirectoryChangedHandle);
			UE_LOG(LogTestPAL, Display, TEXT("Registered callback for changes in '%s'"), *TestDir);
		}
		else
		{
			UE_LOG(LogTestPAL, Fatal, TEXT("Could not get DirectoryWatcher module"));
		}

		FPlatformProcess::Sleep(1.0f);
		DirectoryWatcher->Tick(1.0f);

		// create and remove directory
		UE_LOG(LogTestPAL, Display, TEXT("Creating DIRECTORY '%s'"), *(TestDir + TEXT("/test")));
		verify(PlatformFile.CreateDirectory(*(TestDir + TEXT("/test"))));
		DirectoryWatcher->Tick(1.0f);
		FPlatformProcess::Sleep(1.0f);
		DirectoryWatcher->Tick(1.0f);

		UE_LOG(LogTestPAL, Display, TEXT("Deleting DIRECTORY '%s'"), *(TestDir + TEXT("/test")));
		verify(PlatformFile.DeleteDirectory(*(TestDir + TEXT("/test"))));
		DirectoryWatcher->Tick(1.0f);
		FPlatformProcess::Sleep(1.0f);
		DirectoryWatcher->Tick(1.0f);

		// create and remove in a sub directory
		UE_LOG(LogTestPAL, Display, TEXT("Creating DIRECTORY '%s'"), *(TestDir + TEXT("/subtest/blah")));
		verify(PlatformFile.CreateDirectory(*(TestDir + TEXT("/subtest/blah"))));
		DirectoryWatcher->Tick(1.0f);
		FPlatformProcess::Sleep(1.0f);
		DirectoryWatcher->Tick(1.0f);

		UE_LOG(LogTestPAL, Display, TEXT("Deleting DIRECTORY '%s'"), *(TestDir + TEXT("/subtest/blah")));
		verify(PlatformFile.DeleteDirectory(*(TestDir + TEXT("/subtest/blah"))));
		DirectoryWatcher->Tick(1.0f);
		FPlatformProcess::Sleep(1.0f);
		DirectoryWatcher->Tick(1.0f);

		{
			// create file
			FString DummyFileName = TestDir + TEXT("/test file.bin");
			UE_LOG(LogTestPAL, Display, TEXT("Creating FILE '%s'"), *DummyFileName);
			IFileHandle* DummyFile = PlatformFile.OpenWrite(*DummyFileName);
			check(DummyFile);
			DirectoryWatcher->Tick(1.0f);
			FPlatformProcess::Sleep(1.0f);
			DirectoryWatcher->Tick(1.0f);

			// modify file
			UE_LOG(LogTestPAL, Display, TEXT("Modifying FILE '%s'"), *DummyFileName);
			uint8 Contents = 0;
			DummyFile->Write(&Contents, sizeof(Contents));
			DirectoryWatcher->Tick(1.0f);
			FPlatformProcess::Sleep(1.0f);
			DirectoryWatcher->Tick(1.0f);

			// close the file
			UE_LOG(LogTestPAL, Display, TEXT("Closing FILE '%s'"), *DummyFileName);
			delete DummyFile;
			DummyFile = nullptr;
			DirectoryWatcher->Tick(1.0f);
			FPlatformProcess::Sleep(1.0f);
			DirectoryWatcher->Tick(1.0f);

			// delete file
			UE_LOG(LogTestPAL, Display, TEXT("Deleting FILE '%s'"), *DummyFileName);
			verify(PlatformFile.DeleteFile(*DummyFileName));
			DirectoryWatcher->Tick(1.0f);
			FPlatformProcess::Sleep(1.0f);
			DirectoryWatcher->Tick(1.0f);
		}

		// now the same in a grandchild directory
		{
			FString GrandChildDir = TestDir + TEXT("/subtest/grandchild");

			UE_LOG(LogTestPAL, Display, TEXT("Creating DIRECTORY '%s'"), *GrandChildDir);
			verify(PlatformFile.CreateDirectory(*GrandChildDir));
			DirectoryWatcher->Tick(1.0f);
			FPlatformProcess::Sleep(1.0f);
			DirectoryWatcher->Tick(1.0f);

			{
				// create file
				FString DummyFileName = GrandChildDir + TEXT("/test file.bin");
				UE_LOG(LogTestPAL, Display, TEXT("Creating FILE '%s'"), *DummyFileName);
				IFileHandle* DummyFile = PlatformFile.OpenWrite(*DummyFileName);
				check(DummyFile);
				DirectoryWatcher->Tick(1.0f);
				FPlatformProcess::Sleep(1.0f);
				DirectoryWatcher->Tick(1.0f);

				// modify file
				UE_LOG(LogTestPAL, Display, TEXT("Modifying FILE '%s'"), *DummyFileName);
				uint8 Contents = 0;
				DummyFile->Write(&Contents, sizeof(Contents));
				DirectoryWatcher->Tick(1.0f);
				FPlatformProcess::Sleep(1.0f);
				DirectoryWatcher->Tick(1.0f);

				// close the file
				UE_LOG(LogTestPAL, Display, TEXT("Closing FILE '%s'"), *DummyFileName);
				delete DummyFile;
				DummyFile = nullptr;
				DirectoryWatcher->Tick(1.0f);
				FPlatformProcess::Sleep(1.0f);
				DirectoryWatcher->Tick(1.0f);

				// delete file
				UE_LOG(LogTestPAL, Display, TEXT("Deleting FILE '%s'"), *DummyFileName);
				PlatformFile.DeleteFile(*DummyFileName);
				DirectoryWatcher->Tick(1.0f);
				FPlatformProcess::Sleep(1.0f);
				DirectoryWatcher->Tick(1.0f);
			}

			UE_LOG(LogTestPAL, Display, TEXT("Deleting DIRECTORY '%s'"), *GrandChildDir);
			verify(PlatformFile.DeleteDirectory(*GrandChildDir));
			DirectoryWatcher->Tick(1.0f);
			FPlatformProcess::Sleep(1.0f);
			DirectoryWatcher->Tick(1.0f);
		}


		// clean up
		verify(DirectoryWatcher->UnregisterDirectoryChangedCallback_Handle(TestDir, DirectoryChangedHandle));
		// remove dirs as well
		verify(PlatformFile.DeleteDirectory(*(TestDir + TEXT("/subtest"))));
		verify(PlatformFile.DeleteDirectory(*TestDir));

		UE_LOG(LogTestPAL, Display, TEXT("End of test"));
	}
	else
	{
		UE_LOG(LogTestPAL, Fatal, TEXT("Could not create test directory %s."), *TestDir);
	}

	FEngineLoop::AppPreExit();
	FEngineLoop::AppExit();
	return 0;
}