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

754 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Linux/DirectoryWatchRequestLinux.h"
#include "HAL/FileManager.h"
#include "DirectoryWatcherPrivate.h"
#if !UE_BUILD_SHIPPING
#include "Misc/CoreMisc.h"
#endif
// To see inotify watch events:
// TestPAL dirwatcher -LogCmds="LogDirectoryWatcher VeryVerbose"
#define EVENT_SIZE ( sizeof(struct inotify_event) )
#define EVENT_BUF_LEN ( 1024 * ( EVENT_SIZE + 16 ) )
#define VERBOSE_STATS 1
static bool GDumpStats = false;
static bool GDumpedError = false;
static FString GINotifyErrorMsg;
int FDirectoryWatchRequestLinux::GFileDescriptor = -1;
TMultiMap<int32, FDirectoryWatchRequestLinux::FWatchInfo> FDirectoryWatchRequestLinux::GWatchDescriptorsToWatchInfo;
static uint32 GetPathNameHash(const FString& Key)
{
const TCHAR* Str = &Key[0];
uint32 StrLen = sizeof(TCHAR) * Key.Len();
return CityHash64(reinterpret_cast<const char*>(Str), StrLen);
}
FDirectoryWatchRequestLinux::FDirectoryWatchRequestLinux()
: bWatchSubtree(false)
, bEndWatchRequestInvoked(false)
{
}
FDirectoryWatchRequestLinux::~FDirectoryWatchRequestLinux()
{
Shutdown();
}
void FDirectoryWatchRequestLinux::Shutdown()
{
// Go through all watch descriptors
for (auto MapIt = GWatchDescriptorsToWatchInfo.CreateIterator(); MapIt; ++MapIt)
{
// Check if this one is ours
if (&MapIt->Value.WatchRequest == this)
{
int WatchDescriptor = MapIt->Key;
// Remove this entry
MapIt.RemoveCurrent();
// If this was last watch descriptor for this directory, rm the inotify watch.
if (!GWatchDescriptorsToWatchInfo.Contains(WatchDescriptor))
{
UE_LOG(LogDirectoryWatcher, VeryVerbose, TEXT("- inotify_rm_watch(%d)"), WatchDescriptor);
inotify_rm_watch(GFileDescriptor, WatchDescriptor);
}
}
}
PathNameHashSet.Empty();
if (GWatchDescriptorsToWatchInfo.IsEmpty() && (GFileDescriptor != -1))
{
close(GFileDescriptor);
GFileDescriptor = -1;
}
}
bool FDirectoryWatchRequestLinux::Init(const FString& InDirectory, uint32 Flags)
{
checkf(IsInGameThread(), TEXT("INotify operations only support on main thread"));
if (InDirectory.Len() == 0)
{
// Verify input
return false;
}
Shutdown();
// Make sure the path is absolute
WatchDirectory = FPaths::ConvertRelativePathToFull(InDirectory);
bWatchSubtree = (Flags & IDirectoryWatcher::WatchOptions::IgnoreChangesInSubtree) == 0;
bEndWatchRequestInvoked = false;
UE_LOG(LogDirectoryWatcher, Verbose, TEXT("Adding watch for directory tree '%s'"), *WatchDirectory);
if (GFileDescriptor == -1)
{
GFileDescriptor = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (GFileDescriptor == -1)
{
if (errno == EMFILE)
{
SetINotifyErrorMsg(TEXT("Failed to init inotify (ran out of inotify instances)"));
}
else
{
UE_LOG(LogDirectoryWatcher, Error, TEXT("Failed to init inotify (errno=%d, %s)"), errno, UTF8_TO_TCHAR(strerror(errno)));
}
return false;
}
}
// Find all subdirs and add inotify watch requests
WatchDirectoryTree(WatchDirectory, nullptr);
return true;
}
FDelegateHandle FDirectoryWatchRequestLinux::AddDelegate(const IDirectoryWatcher::FDirectoryChanged& InDelegate, uint32 Flags)
{
Delegates.Emplace(InDelegate, Flags);
return Delegates.Last().Key.GetHandle();
}
bool FDirectoryWatchRequestLinux::RemoveDelegate(FDelegateHandle InHandle)
{
return Delegates.RemoveAll([=](const FWatchDelegate& Delegate) {
return Delegate.Key.GetHandle() == InHandle;
}) != 0;
}
bool FDirectoryWatchRequestLinux::HasDelegates() const
{
return Delegates.Num() > 0;
}
void FDirectoryWatchRequestLinux::EndWatchRequest()
{
bEndWatchRequestInvoked = true;
}
void FDirectoryWatchRequestLinux::ProcessNotifications(TMap<FString, FDirectoryWatchRequestLinux*>& RequestMap)
{
checkf(IsInGameThread(), TEXT("INotify operations only support on main thread"));
ProcessAllINotifyChanges();
// Trigger any file change notification delegates
for (auto MapIt = RequestMap.CreateConstIterator(); MapIt; ++MapIt)
{
FDirectoryWatchRequestLinux &WatchRequest = *MapIt.Value();
WatchRequest.ProcessPendingNotifications();
}
DumpINotifyErrorDetails(RequestMap);
}
void FDirectoryWatchRequestLinux::DumpStats(TMap<FString, FDirectoryWatchRequestLinux*>& RequestMap)
{
GDumpStats = true;
DumpINotifyErrorDetails(RequestMap);
}
void FDirectoryWatchRequestLinux::ProcessPendingNotifications()
{
// Trigger all listening delegates with the files that have changed
if (FileChanges.Num() > 0)
{
TMap<uint32, TArray<FFileChangeData>> FileChangeCache;
for (const FWatchDelegate& Delegate : Delegates)
{
// Filter list of all file changes down to ones that just match this delegate's flags
TArray<FFileChangeData>* CachedChanges = FileChangeCache.Find(Delegate.Value);
if (CachedChanges)
{
Delegate.Key.Execute(*CachedChanges);
}
else
{
const bool bIncludeDirs = (Delegate.Value & IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges) != 0;
TArray<FFileChangeData>& Changes = FileChangeCache.Add(Delegate.Value);
for (const TPair<FFileChangeData, bool>& FileChangeData : FileChanges)
{
if (!FileChangeData.Value || bIncludeDirs)
{
Changes.Add(FileChangeData.Key);
}
}
Delegate.Key.Execute(Changes);
}
}
FileChanges.Empty();
}
}
void FDirectoryWatchRequestLinux::WatchDirectoryTree(const FString & RootAbsolutePath, TArray<TPair<FFileChangeData, bool>>* FileChangesPtr)
{
checkf(IsInGameThread(), TEXT("INotify operations only support on main thread"));
if (bEndWatchRequestInvoked || (GFileDescriptor == -1))
{
return;
}
// If this isn't our root watch directory or under it, don't watch
if (!RootAbsolutePath.StartsWith(WatchDirectory, ESearchCase::CaseSensitive))
{
return;
}
if (FileChangesPtr)
{
FileChangesPtr->Emplace(FFileChangeData(RootAbsolutePath, FFileChangeData::FCA_Added), true);
}
if (!bWatchSubtree && (RootAbsolutePath != WatchDirectory))
{
return;
}
UE_LOG(LogDirectoryWatcher, VeryVerbose, TEXT("Watching tree '%s'"), *RootAbsolutePath);
TArray<FString> AllFiles;
if (bWatchSubtree)
{
IPlatformFile::GetPlatformPhysical().IterateDirectoryRecursively(*RootAbsolutePath,
[&AllFiles, FileChangesPtr](const TCHAR* Name, bool bIsDirectory)
{
if (bIsDirectory)
{
AllFiles.Add(Name);
}
if (FileChangesPtr)
{
FileChangesPtr->Emplace(FFileChangeData(Name, FFileChangeData::FCA_Added), bIsDirectory);
}
return true;
});
}
// Add root path
AllFiles.Add(RootAbsolutePath);
for (const FString& FolderName: AllFiles)
{
uint32 PathNameHash = GetPathNameHash(FolderName);
// Check if we're already watching this directory
if (!PathNameHashSet.Contains(PathNameHash))
{
// If we watch a directory twice, it'll return the same Watch Descriptor
int32 NotifyFilter = IN_CREATE | IN_MOVE | IN_MODIFY | IN_DELETE | IN_ONLYDIR;
int32 WatchDescriptor = inotify_add_watch(GFileDescriptor, TCHAR_TO_UTF8(*FolderName), NotifyFilter);
if (WatchDescriptor == -1)
{
// ENOSPC: The user limit on the total number of inotify watches was reached or the kernel failed to allocate a needed resource.
if (errno == ENOSPC)
{
FString ErrorMsg = FString::Printf(
TEXT("inotify_add_watch cannot watch folder %s (Out of inotify watches)"), *FolderName);
SetINotifyErrorMsg(ErrorMsg);
}
else
{
UE_LOG(LogDirectoryWatcher, Warning, TEXT("inotify_add_watch cannot watch folder %s (errno = %d, %s)"),
*FolderName, errno, UTF8_TO_TCHAR(strerror(errno)));
}
}
else
{
UE_LOG(LogDirectoryWatcher, VeryVerbose, TEXT("+ Added WatchDescriptor %d for '%s'"), WatchDescriptor, *FolderName);
// Set the inotify watch descriptor -> folder name mapping
FWatchInfo WatchInfo{ FolderName, *this };
GWatchDescriptorsToWatchInfo.Add(WatchDescriptor, WatchInfo);
// Add hashed directory path
PathNameHashSet.Add(PathNameHash);
}
}
}
}
void FDirectoryWatchRequestLinux::UnwatchDirectoryTree(const FString& RootAbsolutePath)
{
checkf(IsInGameThread(), TEXT("INotify operations only support on main thread"));
UE_LOG(LogDirectoryWatcher, VeryVerbose, TEXT("Unwatching tree '%s'"), *RootAbsolutePath);
for (auto MapIt = GWatchDescriptorsToWatchInfo.CreateIterator(); MapIt; ++MapIt)
{
int WatchDescriptor = MapIt->Key;
const FWatchInfo& WatchInfo = MapIt->Value;
// Check if this one is ours
if (&MapIt->Value.WatchRequest != this)
{
continue;
}
if (WatchInfo.FolderName.StartsWith(RootAbsolutePath, ESearchCase::CaseSensitive))
{
UE_LOG(LogDirectoryWatcher, VeryVerbose, TEXT("- Removing WatchDescriptor %d for '%s'"), WatchDescriptor, *WatchInfo.FolderName);
PathNameHashSet.Remove(GetPathNameHash(WatchInfo.FolderName));
// Safe version of:
// GWatchDescriptorsToWatchInfo.Remove(WatchDescriptor);
MapIt.RemoveCurrent();
// If that was the last reference to this watch descriptor, remove the inotify watch
if (!GWatchDescriptorsToWatchInfo.Contains(WatchDescriptor))
{
// delete the descriptor
int RetVal = inotify_rm_watch(GFileDescriptor, WatchDescriptor);
UE_LOG(LogDirectoryWatcher, VeryVerbose, TEXT("- inotify_rm_watch(%d): %d"), WatchDescriptor, RetVal ? errno : 0);
// This function may be called when root path has been deleted, and inotify_rm_watch() will fail
// with an EINVAL when removing a watch on a deleted file.
if (RetVal == -1 && errno != EINVAL)
{
UE_LOG(LogDirectoryWatcher, Warning, TEXT("inotify_rm_watch cannot remove descriptor %d for folder '%s' (errno = %d, %s)"),
WatchDescriptor, *WatchInfo.FolderName, errno, ANSI_TO_TCHAR(strerror(errno)));
}
}
}
}
}
static FString INotifyFlagsToStr(uint32 INotifyFlags)
{
#if UE_BUILD_SHIPPING
return FString();
#else
FString Ret = TEXT("[");
#define _XTAG(_x) if (INotifyFlags & _x) Ret += FString(TEXT(" ")) + TEXT(#_x)
_XTAG(IN_ACCESS);
_XTAG(IN_MODIFY);
_XTAG(IN_ATTRIB);
_XTAG(IN_CLOSE_WRITE);
_XTAG(IN_CLOSE_NOWRITE);
_XTAG(IN_OPEN);
_XTAG(IN_MOVED_FROM);
_XTAG(IN_MOVED_TO);
_XTAG(IN_CREATE);
_XTAG(IN_DELETE);
_XTAG(IN_DELETE_SELF);
_XTAG(IN_MOVE_SELF);
_XTAG(IN_UNMOUNT);
_XTAG(IN_Q_OVERFLOW);
_XTAG(IN_IGNORED);
_XTAG(IN_ISDIR);
#undef _XTAG
Ret += TEXT(" ]");
return Ret;
#endif
}
void FDirectoryWatchRequestLinux::ProcessNotifyChanges(const FString& FolderName, const struct inotify_event* Event)
{
if (bEndWatchRequestInvoked)
{
return;
}
int WatchDescriptor = Event->wd;
bool bIsDir = (Event->mask & IN_ISDIR) != 0;
FFileChangeData::EFileChangeAction Action = FFileChangeData::FCA_Unknown;
FString AffectedFile = FolderName / UTF8_TO_TCHAR(Event->name);
UE_LOG(LogDirectoryWatcher, VeryVerbose, TEXT("Event: WatchDescriptor %d, mask 0x%08x, EventPath: '%s' Event Name: '%s' Len: %u %s"),
WatchDescriptor, Event->mask, *FolderName, UTF8_TO_TCHAR(Event->name), Event->len, *INotifyFlagsToStr(Event->mask));
if ((Event->mask & IN_CREATE) || (Event->mask & IN_MOVED_TO))
{
// IN_CREATE: File/directory created in watched directory
// IN_MOVED_TO: Generated for the directory containing the new filename when a file is renamed
if (bIsDir)
{
// If a directory was created/moved, watch it and add changes to FileChanges.
// Leave Action as FCA_Unknown so nothing gets added down below.
WatchDirectoryTree(AffectedFile, &FileChanges);
}
else
{
Action = FFileChangeData::FCA_Added;
}
}
else if (Event->mask & IN_MODIFY)
{
// IN_MODIFY: File was modified
// If a directory was modified, we expect to get events from already watched files in it
Action = FFileChangeData::FCA_Modified;
}
// Check if the file/directory itself has been deleted (IGNORED can also be sent on delete)
else if ((Event->mask & IN_DELETE_SELF) || (Event->mask & IN_UNMOUNT))
{
// IN_DELETE_SELF: Watched file/directory was itself deleted.
// In addition, an IN_IGNORED event will subsequently be generated for the watch descriptor
// IN_UNMOUNT: Filesystem containing watched object was unmounted.
// In addition, an IN_IGNORED event will subsequently be generated for the watch descriptor
// If a directory was deleted, we expect to get events from already watched files in it
// NOTE: This code should ever get called - we only watch directories.
checkf(bIsDir, TEXT("Watched item was file?"));
if (bIsDir)
{
UnwatchDirectoryTree(AffectedFile);
Action = FFileChangeData::FCA_Removed;
}
}
else if (Event->mask & IN_IGNORED)
{
// IN_IGNORED: Watch was removed explicitly (inotify_rm_watch) or
// automatically (file was deleted, or filesystem was unmounted).
PathNameHashSet.Remove(GetPathNameHash(FolderName));
GWatchDescriptorsToWatchInfo.Remove(WatchDescriptor);
}
else if ((Event->mask & IN_DELETE) || (Event->mask & IN_MOVED_FROM))
{
// IN_DELETE: File/directory deleted from watched directory
// IN_MOVED_FROM: Generated for the directory containing the old filename when a file is renamed
// If a directory was deleted/moved, unwatch it
if (bIsDir)
{
UnwatchDirectoryTree(AffectedFile);
}
Action = FFileChangeData::FCA_Removed;
}
if (Action != FFileChangeData::FCA_Unknown)
{
FileChanges.Emplace(FFileChangeData(AffectedFile, Action), bIsDir);
}
}
void FDirectoryWatchRequestLinux::ProcessAllINotifyChanges()
{
uint8_t Buffer[EVENT_BUF_LEN] __attribute__ ((aligned(__alignof__(struct inotify_event))));
if (GFileDescriptor == -1)
{
return;
}
// Loop while events can be read from inotify file descriptor
for (;;)
{
// Read event stream
ssize_t Len = read(GFileDescriptor, Buffer, EVENT_BUF_LEN);
// If the non-blocking read() found no events to read, then it returns -1 with errno set to EAGAIN.
if (Len == -1 && errno != EAGAIN)
{
UE_LOG(LogDirectoryWatcher, Error, TEXT("FDirectoryWatchRequestLinux::ProcessAllINotifyChanges() read() error (errno = %d, %s)"),
errno, ANSI_TO_TCHAR(strerror(errno)));
break;
}
if (Len <= 0)
{
break;
}
// Loop over all events in the buffer
uint8_t* Ptr = Buffer;
while (Ptr < Buffer + Len)
{
const struct inotify_event* Event;
Event = reinterpret_cast<const struct inotify_event *>(Ptr);
Ptr += EVENT_SIZE + Event->len;
// Skip if overflowed
if ((Event->wd != -1) && (Event->mask & IN_Q_OVERFLOW) == 0)
{
TArray<FWatchInfo> WatchInfos;
GWatchDescriptorsToWatchInfo.MultiFind(Event->wd, WatchInfos);
for (FWatchInfo& WatchInfo : WatchInfos)
{
WatchInfo.WatchRequest.ProcessNotifyChanges(WatchInfo.FolderName, Event);
}
}
}
}
}
static uint32 get_inotify_procfs_value(const char *FileName)
{
char Buf[256];
uint32 InterfaceVal = 0;
snprintf(Buf, sizeof(Buf), "/proc/sys/fs/inotify/%s", FileName);
Buf[sizeof(Buf) - 1] = 0;
FILE* FilePtr = fopen(Buf, "r");
if (FilePtr)
{
if (fscanf(FilePtr, "%u", &InterfaceVal) != 1)
{
InterfaceVal = 0;
}
fclose(FilePtr);
}
return InterfaceVal;
}
void FDirectoryWatchRequestLinux::SetINotifyErrorMsg(const FString &ErrorMsg)
{
if (!GINotifyErrorMsg.Len())
{
GINotifyErrorMsg = ErrorMsg;
}
}
static FString GetLinkName(const char *Pathname)
{
FString Result;
char Filename[PATH_MAX + 1];
ssize_t Ret = readlink(Pathname, Filename, sizeof(Filename));
if ((Ret > 0) && (Ret < sizeof(Filename)))
{
Filename[Ret] = 0;
Result = Filename;
}
return Result;
}
static uint32 INotifyParseFDInfoFile(const FString& Executable, int Pid, const char *d_name)
{
uint32 INotifyCount = 0;
FILE* FilePtr = fopen(d_name, "r");
if (FilePtr)
{
char line_buf[256];
for (;;)
{
if (!fgets(line_buf, sizeof(line_buf), FilePtr))
{
break;
}
if (!strncmp(line_buf, "inotify ", 8))
{
INotifyCount++;
}
}
fclose(FilePtr);
}
return INotifyCount;
}
static void INotifyParseFDDir(const FString& Executable, int Pid, uint32 &INotifyCountTotal, uint32& INotifyInstancesTotal)
{
char Buf[256];
uint32 INotifyCount = 0;
uint32 INotifyInstances = 0;
snprintf(Buf, sizeof(Buf), "/proc/%d/fd", Pid);
Buf[sizeof(Buf) - 1] = 0;
DIR* dir_fd = opendir(Buf);
if (dir_fd)
{
for (;;)
{
struct dirent* dp_fd = readdir(dir_fd);
if (!dp_fd)
{
break;
}
if ((dp_fd->d_type == DT_LNK) && isdigit(dp_fd->d_name[0]))
{
snprintf(Buf, sizeof(Buf), "/proc/%d/fd/%s", Pid, dp_fd->d_name);
Buf[sizeof(Buf) - 1] = 0;
FString Filename = GetLinkName(Buf);
if (Filename == TEXT("anon_inode:inotify"))
{
snprintf(Buf, sizeof(Buf), "/proc/%d/fdinfo/%s", Pid, dp_fd->d_name);
Buf[sizeof(Buf) - 1] = 0;
uint32 Count = INotifyParseFDInfoFile(Executable, Pid, Buf);
if (Count)
{
INotifyInstances++;
INotifyCount += Count;
}
}
}
}
closedir(dir_fd);
}
if (INotifyCount)
{
FString ExeName = FPaths::GetCleanFilename(Executable);
#if !VERBOSE_STATS
if (Pid == getpid())
#endif
{
UE_LOG(LogDirectoryWatcher, Warning, TEXT(" %s (pid %d) watches:%u instances:%u"), *ExeName, Pid, INotifyCount, INotifyInstances);
}
INotifyCountTotal += INotifyCount;
INotifyInstancesTotal += INotifyInstances;
}
}
static void INotifyDumpProcessStats()
{
uint32 INotifyCountTotal = 0;
uint32 INotifyInstancesTotal = 0;
DIR* dir_proc = opendir("/proc");
if (dir_proc)
{
for (;;)
{
struct dirent* dp_proc = readdir(dir_proc);
if (!dp_proc)
{
break;
}
if ((dp_proc->d_type == DT_DIR) && isdigit(dp_proc->d_name[0]))
{
char Buf[256];
int32 Pid = atoi(dp_proc->d_name);
snprintf(Buf, sizeof(Buf), "/proc/%d/exe", Pid);
Buf[sizeof(Buf) - 1] = 0;
FString Executable = GetLinkName(Buf);
if (Executable.Len())
{
INotifyParseFDDir(Executable, Pid, INotifyCountTotal, INotifyInstancesTotal);
}
}
}
closedir(dir_proc);
}
UE_LOG(LogDirectoryWatcher, Warning, TEXT("Total inotify Watches:%u Instances:%u"), INotifyCountTotal, INotifyInstancesTotal);
}
void FDirectoryWatchRequestLinux::DumpINotifyErrorDetails(TMap<FString, FDirectoryWatchRequestLinux*>& RequestMap)
{
if (!GDumpStats)
{
if (GDumpedError || !GINotifyErrorMsg.Len())
{
return;
}
GDumpedError = true;
UE_LOG(LogDirectoryWatcher, Warning, TEXT("inotify error: %s"), *GINotifyErrorMsg);
}
GDumpStats = false;
#if VERBOSE_STATS
uint32 MaxQueuedEvents = get_inotify_procfs_value("max_queued_events");
uint32 MaxUserInstances = get_inotify_procfs_value("max_user_instances");
uint32 MaxUserWatches = get_inotify_procfs_value("max_user_watches");
UE_LOG(LogDirectoryWatcher, Warning, TEXT("inotify limits"));
UE_LOG(LogDirectoryWatcher, Warning, TEXT(" max_queued_events: %u"), MaxQueuedEvents);
UE_LOG(LogDirectoryWatcher, Warning, TEXT(" max_user_instances: %u"), MaxUserInstances);
UE_LOG(LogDirectoryWatcher, Warning, TEXT(" max_user_watches: %u"), MaxUserWatches);
#endif
UE_LOG(LogDirectoryWatcher, Warning, TEXT("inotify per-process stats"));
INotifyDumpProcessStats();
uint32 Count = 0;
UE_LOG(LogDirectoryWatcher, Warning, TEXT("Current watch requests"));
for (auto MapIt = RequestMap.CreateConstIterator(); MapIt; ++MapIt)
{
uint32 DirCount = 1;
FDirectoryWatchRequestLinux &WatchRequest = *MapIt.Value();
if (!IPlatformFile::GetPlatformPhysical().DirectoryExists(*WatchRequest.WatchDirectory))
{
UE_LOG(LogDirectoryWatcher, Warning, TEXT(" %s: %u watches (dir does not exist)"),
*WatchRequest.WatchDirectory, WatchRequest.PathNameHashSet.Num());
}
else
{
// Get actual count of subdirectories
if (WatchRequest.bWatchSubtree)
{
IPlatformFile::GetPlatformPhysical().IterateDirectoryRecursively(*WatchRequest.WatchDirectory,
[&DirCount](const TCHAR* Name, bool bIsDirectory)
{
DirCount += bIsDirectory;
return true;
});
}
UE_LOG(LogDirectoryWatcher, Warning, TEXT(" %s: %u watches (%u total dirs)"),
*WatchRequest.WatchDirectory, WatchRequest.PathNameHashSet.Num(), DirCount);
Count += WatchRequest.PathNameHashSet.Num();
}
}
UE_LOG(LogDirectoryWatcher, Warning, TEXT("Total UE inotify Watches:%u WatchDescriptors:%u Instances:%u "),
Count, GWatchDescriptorsToWatchInfo.Num(), RequestMap.Num());
}
#if !UE_BUILD_SHIPPING
static bool INotifyCommandHandler(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar)
{
if (FParse::Command(&Cmd, TEXT("DumpINotifyStats")))
{
UE_LOG(LogDirectoryWatcher, Warning, TEXT("Dumping inotify stats"));
GDumpStats = true;
return true;
}
return false;
}
FStaticSelfRegisteringExec FDirectoryWatchRequestLinuxExecs(INotifyCommandHandler);
#endif