357 lines
11 KiB
C++
357 lines
11 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "DirectoryWatchRequestWindows.h"
|
|
|
|
#include "DirectoryWatcherPrivate.h"
|
|
#include "Misc/DateTime.h"
|
|
|
|
FDirectoryWatchRequestWindows::FDirectoryWatchRequestWindows(uint32 Flags)
|
|
{
|
|
bPendingDelete = false;
|
|
bEndWatchRequestInvoked = false;
|
|
WatchStartedTimeStampHistory[0] = 0;
|
|
WatchStartedTimeStampHistory[1] = 0;
|
|
|
|
bWatchSubtree = (Flags & IDirectoryWatcher::WatchOptions::IgnoreChangesInSubtree) == 0;
|
|
bool bIncludeDirectoryEvents = (Flags & IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges) != 0;
|
|
|
|
NotifyFilter = FILE_NOTIFY_CHANGE_FILE_NAME | (bIncludeDirectoryEvents? FILE_NOTIFY_CHANGE_DIR_NAME : 0) | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION;
|
|
|
|
DirectoryHandle = INVALID_HANDLE_VALUE;
|
|
|
|
constexpr int32 InitialMaxChanges = 16384;
|
|
SetBufferByChangeCount(InitialMaxChanges);
|
|
|
|
FMemory::Memzero(&Overlapped, sizeof(Overlapped));
|
|
|
|
Overlapped.hEvent = this;
|
|
}
|
|
|
|
FDirectoryWatchRequestWindows::~FDirectoryWatchRequestWindows()
|
|
{
|
|
if ( DirectoryHandle != INVALID_HANDLE_VALUE )
|
|
{
|
|
::CloseHandle(DirectoryHandle);
|
|
DirectoryHandle = INVALID_HANDLE_VALUE;
|
|
bBufferInUse = false;
|
|
}
|
|
}
|
|
|
|
void FDirectoryWatchRequestWindows::SetBufferByChangeCount(int32 MaxChanges)
|
|
{
|
|
checkf(!bBufferInUse, TEXT("Reallocating the buffer while it is referenced in a pending ReadDirectoryChangesW call is invalid and will likely cause a crash."));
|
|
BufferLength = sizeof(FILE_NOTIFY_INFORMATION) * MaxChanges;
|
|
Buffer.Reset(reinterpret_cast<uint8*>(FMemory::Malloc(BufferLength, alignof(DWORD))));
|
|
BackBuffer.Reset(reinterpret_cast<uint8*>(FMemory::Malloc(BufferLength, alignof(DWORD))));
|
|
FMemory::Memzero(Buffer.Get(), BufferLength);
|
|
FMemory::Memzero(BackBuffer.Get(), BufferLength);
|
|
}
|
|
|
|
void FDirectoryWatchRequestWindows::SetBufferBySize(int32 BufferSize)
|
|
{
|
|
int32 MaxChanges = BufferSize / sizeof(FILE_NOTIFY_INFORMATION); // Arbitrarily we choose to round down
|
|
SetBufferByChangeCount(MaxChanges);
|
|
}
|
|
|
|
bool FDirectoryWatchRequestWindows::Init(const FString& InDirectory)
|
|
{
|
|
check(Buffer);
|
|
|
|
if ( InDirectory.Len() == 0 )
|
|
{
|
|
// Verify input
|
|
return false;
|
|
}
|
|
|
|
Directory = InDirectory;
|
|
|
|
if ( DirectoryHandle != INVALID_HANDLE_VALUE )
|
|
{
|
|
// If we already had a handle for any reason, close the old handle
|
|
::CloseHandle(DirectoryHandle);
|
|
}
|
|
|
|
// Make sure the path is absolute
|
|
const FString FullPath = FPaths::ConvertRelativePathToFull(Directory);
|
|
|
|
// Get a handle to the directory with FILE_FLAG_BACKUP_SEMANTICS as per remarks for ReadDirectoryChanges on MSDN
|
|
DirectoryHandle = ::CreateFile(
|
|
*FullPath,
|
|
FILE_LIST_DIRECTORY,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
NULL,
|
|
OPEN_EXISTING,
|
|
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
|
|
NULL
|
|
);
|
|
|
|
if ( DirectoryHandle == INVALID_HANDLE_VALUE )
|
|
{
|
|
const DWORD ErrorCode = ::GetLastError();
|
|
// Failed to obtain a handle to this directory
|
|
UE_LOG(LogDirectoryWatcher, Display, TEXT("CreateFile failed for '%s'. GetLastError code [%d]"), *FullPath, ErrorCode);
|
|
return false;
|
|
}
|
|
|
|
const bool bSuccess = !!::ReadDirectoryChangesW(
|
|
DirectoryHandle,
|
|
Buffer.Get(),
|
|
BufferLength,
|
|
bWatchSubtree,
|
|
NotifyFilter,
|
|
NULL,
|
|
&Overlapped,
|
|
&FDirectoryWatchRequestWindows::ChangeNotification);
|
|
|
|
if ( !bSuccess )
|
|
{
|
|
const DWORD ErrorCode = ::GetLastError();
|
|
UE_LOG(LogDirectoryWatcher, Display, TEXT("Initial ReadDirectoryChangesW failed for '%s'. GetLastError code [%d]"), *Directory, ErrorCode);
|
|
::CloseHandle(DirectoryHandle);
|
|
DirectoryHandle = INVALID_HANDLE_VALUE;
|
|
return false;
|
|
}
|
|
WatchStartedTimeStampHistory[0] = FDateTime::UtcNow().ToUnixTimestamp();
|
|
WatchStartedTimeStampHistory[1] = WatchStartedTimeStampHistory[0];
|
|
bBufferInUse = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
FDelegateHandle FDirectoryWatchRequestWindows::AddDelegate( const IDirectoryWatcher::FDirectoryChanged& InDelegate )
|
|
{
|
|
Delegates.Add(InDelegate);
|
|
return Delegates.Last().GetHandle();
|
|
}
|
|
|
|
bool FDirectoryWatchRequestWindows::RemoveDelegate( FDelegateHandle InHandle )
|
|
{
|
|
return Delegates.RemoveAll([=](const IDirectoryWatcher::FDirectoryChanged& Delegate) {
|
|
return Delegate.GetHandle() == InHandle;
|
|
}) != 0;
|
|
}
|
|
|
|
bool FDirectoryWatchRequestWindows::HasDelegates() const
|
|
{
|
|
return Delegates.Num() > 0;
|
|
}
|
|
|
|
HANDLE FDirectoryWatchRequestWindows::GetDirectoryHandle() const
|
|
{
|
|
return DirectoryHandle;
|
|
}
|
|
|
|
void FDirectoryWatchRequestWindows::EndWatchRequest()
|
|
{
|
|
if ( !bEndWatchRequestInvoked && !bPendingDelete )
|
|
{
|
|
if ( DirectoryHandle != INVALID_HANDLE_VALUE )
|
|
{
|
|
CancelIoEx(DirectoryHandle, &Overlapped);
|
|
bBufferInUse = false;
|
|
// Clear the handle so we don't setup any more requests, and wait for the operation to finish
|
|
HANDLE TempDirectoryHandle = DirectoryHandle;
|
|
DirectoryHandle = INVALID_HANDLE_VALUE;
|
|
WaitForSingleObjectEx(TempDirectoryHandle, 1000, true);
|
|
|
|
::CloseHandle(TempDirectoryHandle);
|
|
}
|
|
else
|
|
{
|
|
// The directory handle was never opened
|
|
bPendingDelete = true;
|
|
}
|
|
|
|
// Only allow this to be invoked once
|
|
bEndWatchRequestInvoked = true;
|
|
}
|
|
}
|
|
|
|
void FDirectoryWatchRequestWindows::ProcessPendingNotifications()
|
|
{
|
|
// Trigger all listening delegates with the files that have changed
|
|
if ( FileChanges.Num() > 0 )
|
|
{
|
|
for (int32 DelegateIdx = 0; DelegateIdx < Delegates.Num(); ++DelegateIdx)
|
|
{
|
|
if (Delegates[DelegateIdx].IsBound())
|
|
{
|
|
Delegates[DelegateIdx].Execute(FileChanges);
|
|
}
|
|
else
|
|
{
|
|
Delegates.RemoveAt(DelegateIdx--);
|
|
}
|
|
}
|
|
|
|
FileChanges.Empty();
|
|
}
|
|
}
|
|
|
|
void FDirectoryWatchRequestWindows::ProcessChange(uint32 Error, uint32 NumBytes)
|
|
{
|
|
bBufferInUse = false; // Buffer reallocations are allowed in this handling code before we resubmit the watch
|
|
auto CloseHandleAndMarkForDelete = [this]()
|
|
{
|
|
::CloseHandle(DirectoryHandle);
|
|
DirectoryHandle = INVALID_HANDLE_VALUE;
|
|
bPendingDelete = true;
|
|
};
|
|
|
|
if (Error == 0 && NumBytes == 0)
|
|
{
|
|
DWORD UnusedNumberOfBytes;
|
|
GetOverlappedResult(DirectoryHandle, &Overlapped, &UnusedNumberOfBytes, 0);
|
|
Error = ::GetLastError();
|
|
}
|
|
|
|
if (Error == ERROR_OPERATION_ABORTED)
|
|
{
|
|
// The operation was aborted, likely due to EndWatchRequest canceling it.
|
|
// Mark the request for delete so it can be cleaned up next tick.
|
|
bPendingDelete = true;
|
|
UE_CLOG(!IsEngineExitRequested(), LogDirectoryWatcher, Log, TEXT("A directory notification for '%s' was aborted."), *Directory);
|
|
return;
|
|
}
|
|
bool bValidNotification = Error != ERROR_IO_INCOMPLETE && NumBytes > 0;
|
|
bool bIsRescan = false;
|
|
int64 RescanReportTimestamp = 0;
|
|
if (bValidNotification)
|
|
{
|
|
// Swap the pointer to the backbuffer so we can start a new read as soon as possible
|
|
Swap(Buffer, BackBuffer);
|
|
check(Buffer && BackBuffer);
|
|
}
|
|
else if (Error == ERROR_INVALID_PARAMETER)
|
|
{
|
|
// ReadDirectoryChangesW fails with ERROR_INVALID_PARAMETER when the buffer length is greater than 64 KB and the application is
|
|
// monitoring a directory over the network. This is due to a packet size limitation with the underlying file sharing protocols.
|
|
constexpr int32 MaxAllowedSize = 64 * 1024;
|
|
if (BufferLength > MaxAllowedSize)
|
|
{
|
|
// This error is expected, and is sent before we fail to read changes, so do not log it
|
|
SetBufferBySize(MaxAllowedSize);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDirectoryWatcher, Display, TEXT("A directory notification failed for '%s' with ERROR_INVALID_PARAMETER. Attempting another request..."), *Directory);
|
|
}
|
|
}
|
|
else if (Error == ERROR_ACCESS_DENIED)
|
|
{
|
|
CloseHandleAndMarkForDelete();
|
|
UE_LOG(LogDirectoryWatcher, Display, TEXT("A directory notification failed for '%s' because it could not be accessed. Aborting watch request..."), *Directory);
|
|
return;
|
|
}
|
|
else if (Error == ERROR_NOTIFY_ENUM_DIR)
|
|
{
|
|
bValidNotification = true;
|
|
bIsRescan = true;
|
|
RescanReportTimestamp = WatchStartedTimeStampHistory[1];
|
|
}
|
|
else if (Error != ERROR_SUCCESS)
|
|
{
|
|
UE_LOG(LogDirectoryWatcher, Display, TEXT("A directory notification failed for '%s' with error code [%d]. Attemping another request..."), *Directory, Error);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDirectoryWatcher, Display, TEXT("A directory notification failed for '%s' due to buffer overflow. Attemping another request..."), *Directory);
|
|
}
|
|
|
|
// Start up another read
|
|
const bool bSuccess = !!::ReadDirectoryChangesW(
|
|
DirectoryHandle,
|
|
Buffer.Get(),
|
|
BufferLength,
|
|
bWatchSubtree,
|
|
NotifyFilter,
|
|
NULL,
|
|
&Overlapped,
|
|
&FDirectoryWatchRequestWindows::ChangeNotification);
|
|
|
|
if ( !bSuccess )
|
|
{
|
|
const DWORD ErrorCode = ::GetLastError();
|
|
UE_LOG(LogDirectoryWatcher, Display, TEXT("Refresh of ReadDirectoryChangesW failed. GetLastError code [%d] Handle [%p], Path [%s]. Aborting watch request..."),
|
|
ErrorCode, DirectoryHandle, *Directory);
|
|
CloseHandleAndMarkForDelete();
|
|
return;
|
|
}
|
|
WatchStartedTimeStampHistory[1] = WatchStartedTimeStampHistory[0];
|
|
WatchStartedTimeStampHistory[0] = FDateTime::UtcNow().ToUnixTimestamp();
|
|
bBufferInUse = true;
|
|
|
|
// No need to process the change if we can not execute any delegates
|
|
if ( !HasDelegates() || !bValidNotification )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!bIsRescan)
|
|
{
|
|
// Process the change
|
|
uint8* InfoBase = BackBuffer.Get();
|
|
do
|
|
{
|
|
FILE_NOTIFY_INFORMATION* NotifyInfo = (FILE_NOTIFY_INFORMATION*)InfoBase;
|
|
|
|
// Copy the WCHAR out of the NotifyInfo so we can put a NULL terminator on it and convert it to a FString
|
|
FString LeafFilename;
|
|
{
|
|
// The Memcpy below assumes that WCHAR and TCHAR are equivalent (which they should be on Windows)
|
|
static_assert(sizeof(WCHAR) == sizeof(TCHAR), "WCHAR is assumed to be the same size as TCHAR on Windows!");
|
|
|
|
const int32 LeafFilenameLen = NotifyInfo->FileNameLength / sizeof(WCHAR);
|
|
LeafFilename.GetCharArray().AddZeroed(LeafFilenameLen + 1);
|
|
FMemory::Memcpy(LeafFilename.GetCharArray().GetData(), NotifyInfo->FileName, NotifyInfo->FileNameLength);
|
|
}
|
|
|
|
FFileChangeData::EFileChangeAction Action;
|
|
switch(NotifyInfo->Action)
|
|
{
|
|
case FILE_ACTION_ADDED:
|
|
case FILE_ACTION_RENAMED_NEW_NAME:
|
|
Action = FFileChangeData::FCA_Added;
|
|
break;
|
|
|
|
case FILE_ACTION_REMOVED:
|
|
case FILE_ACTION_RENAMED_OLD_NAME:
|
|
Action = FFileChangeData::FCA_Removed;
|
|
break;
|
|
|
|
case FILE_ACTION_MODIFIED:
|
|
Action = FFileChangeData::FCA_Modified;
|
|
break;
|
|
|
|
default:
|
|
Action = FFileChangeData::FCA_Unknown;
|
|
break;
|
|
}
|
|
FileChanges.Emplace(Directory / LeafFilename, Action);
|
|
|
|
// If there is not another entry, break the loop
|
|
if ( NotifyInfo->NextEntryOffset == 0 )
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Adjust the offset and update the NotifyInfo pointer
|
|
InfoBase = InfoBase + NotifyInfo->NextEntryOffset;
|
|
}
|
|
while(true);
|
|
}
|
|
else
|
|
{
|
|
FFileChangeData& ChangeData = FileChanges.Emplace_GetRef(Directory, FFileChangeData::FCA_RescanRequired);
|
|
ChangeData.TimeStamp = RescanReportTimestamp;
|
|
}
|
|
}
|
|
|
|
void FDirectoryWatchRequestWindows::ChangeNotification(::DWORD Error, ::DWORD NumBytes, LPOVERLAPPED InOverlapped)
|
|
{
|
|
FDirectoryWatchRequestWindows* Request = (FDirectoryWatchRequestWindows*)InOverlapped->hEvent;
|
|
|
|
check(Request);
|
|
Request->ProcessChange((uint32)Error, (uint32)NumBytes);
|
|
}
|