Files
UnrealEngine/Engine/Source/Developer/DirectoryWatcher/Private/DirectoryWatcherProxy.cpp
2025-05-18 13:04:45 +08:00

165 lines
4.9 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DirectoryWatcherProxy.h"
#include "DirectoryWatcherPrivate.h"
#include "Async/TaskGraphInterfaces.h"
namespace DirectoryWatcherProxyUtil
{
FString GetAbsolutePath(const FString& InDirectory)
{
FString AbsolutePath = FPaths::ConvertRelativePathToFull(InDirectory);
AbsolutePath /= FString(); // Ensure a trailing slash
return AbsolutePath;
}
}
FDirectoryWatcherProxy::FDirectoryWatcherProxy()
: Inner(new FDirectoryWatcher())
, bWatchMapPendingSort(false)
{
}
FDirectoryWatcherProxy::~FDirectoryWatcherProxy()
{
delete Inner;
}
bool FDirectoryWatcherProxy::RegisterDirectoryChangedCallback_Handle(const FString& Directory, const FDirectoryChanged& InDelegate, FDelegateHandle& Handle, uint32 Flags)
{
if (Inner->RegisterDirectoryChangedCallback_Handle(Directory, InDelegate, Handle, Flags))
{
TArray<FWatchCallback>& WatchCallbacks = WatchMap.FindOrAdd(DirectoryWatcherProxyUtil::GetAbsolutePath(Directory));
WatchCallbacks.Add(FWatchCallback{ InDelegate, Handle, Flags });
bWatchMapPendingSort = true;
return true;
}
return false;
}
bool FDirectoryWatcherProxy::UnregisterDirectoryChangedCallback_Handle(const FString& Directory, FDelegateHandle InHandle)
{
const bool bSuccess = Inner->UnregisterDirectoryChangedCallback_Handle(Directory, InHandle);
const FString WatchPath = DirectoryWatcherProxyUtil::GetAbsolutePath(Directory);
if (TArray<FWatchCallback>* WatchCallbacks = WatchMap.Find(WatchPath))
{
WatchCallbacks->RemoveAll([&InHandle](const FWatchCallback& InWatchCallback)
{
return InWatchCallback.InnerHandle == InHandle;
});
if (WatchCallbacks->Num() == 0)
{
WatchMap.Remove(WatchPath);
}
}
return bSuccess;
}
void FDirectoryWatcherProxy::Tick(float DeltaSeconds)
{
Inner->Tick(DeltaSeconds);
ProcessPendingChanges();
}
bool FDirectoryWatcherProxy::DumpStats()
{
return Inner->DumpStats();
}
void FDirectoryWatcherProxy::RegisterExternalChanges(TArrayView<const FFileChangeData> FileChanges)
{
if (IsInGameThread())
{
RegisterExternalChanges_GameThread(FileChanges);
}
else
{
FFunctionGraphTask::CreateAndDispatchWhenReady([this, FileChangesCopy = TArray<FFileChangeData>(FileChanges.GetData(), FileChanges.Num())]()
{
RegisterExternalChanges_GameThread(FileChangesCopy);
}, TStatId(), nullptr, ENamedThreads::GameThread);
}
}
void FDirectoryWatcherProxy::RegisterExternalChanges_GameThread(TArrayView<const FFileChangeData> FileChanges)
{
PendingFileChanges.Append(FileChanges.GetData(), FileChanges.Num());
}
void FDirectoryWatcherProxy::ProcessPendingChanges()
{
if (PendingFileChanges.Num() == 0)
{
return;
}
// Ensure the map is sorted correctly (by path length)
if (bWatchMapPendingSort)
{
WatchMap.KeySort([](const FString& InPathOne, const FString& InPathTwo) -> bool
{
return InPathOne.Len() < InPathTwo.Len();
});
bWatchMapPendingSort = false;
}
TMap<const FWatchCallback*, TArray<FFileChangeData>> PendingNotifies;
// Filter the changes to work out which of the the watchers we should notify
for (const FFileChangeData& FileChange : PendingFileChanges)
{
// Note: FFileChangeData doesn't tell us whether the changed item is a file or directory (Mac and
// Linux know this information, but Windows does not), so this is a crude hack to try and guess
const bool bIsDirectory = FPaths::GetExtension(FileChange.Filename).IsEmpty();
FString FileChangePath = FPaths::ConvertRelativePathToFull(FileChange.Filename);
if (!bIsDirectory)
{
FileChangePath = FPaths::GetPath(MoveTemp(FileChangePath));
}
FileChangePath /= FString(); // Ensure a trailing slash
// Walk the map of watches looking for complete or partial matches
for (const auto& WatchMapPair : WatchMap)
{
const FString& WatchPath = WatchMapPair.Key;
// If this watch path is longer that the change path then we can skip it
if (WatchPath.Len() > FileChangePath.Len())
{
// The map is sorted by path length, so we can bail once we find a watch path longer than our change path
break;
}
// If the change path starts with this watch path then this is something we should potentially notify
if (FileChangePath.StartsWith(WatchPath))
{
const bool bIsParentPath = WatchPath.Len() < FileChangePath.Len();
for (const FWatchCallback& WatchCallback : WatchMapPair.Value)
{
// Should we notify this path based on its flags?
if ((!bIsParentPath || (WatchCallback.WatchFlags & IDirectoryWatcher::WatchOptions::IgnoreChangesInSubtree) == 0) &&
(!bIsDirectory || (WatchCallback.WatchFlags & IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges) != 0)
)
{
TArray<FFileChangeData>& PendingNotifyFileChanges = PendingNotifies.FindOrAdd(&WatchCallback);
PendingNotifyFileChanges.Add(FileChange);
}
}
}
}
}
PendingFileChanges.Reset();
// Notify everything
for (const auto& PendingNotifyPair : PendingNotifies)
{
PendingNotifyPair.Key->Delegate.ExecuteIfBound(PendingNotifyPair.Value);
}
}