// 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 FDirectoryWatchRequestLinux::GWatchDescriptorsToWatchInfo; static uint32 GetPathNameHash(const FString& Key) { const TCHAR* Str = &Key[0]; uint32 StrLen = sizeof(TCHAR) * Key.Len(); return CityHash64(reinterpret_cast(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& 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& RequestMap) { GDumpStats = true; DumpINotifyErrorDetails(RequestMap); } void FDirectoryWatchRequestLinux::ProcessPendingNotifications() { // Trigger all listening delegates with the files that have changed if (FileChanges.Num() > 0) { TMap> FileChangeCache; for (const FWatchDelegate& Delegate : Delegates) { // Filter list of all file changes down to ones that just match this delegate's flags TArray* CachedChanges = FileChangeCache.Find(Delegate.Value); if (CachedChanges) { Delegate.Key.Execute(*CachedChanges); } else { const bool bIncludeDirs = (Delegate.Value & IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges) != 0; TArray& Changes = FileChangeCache.Add(Delegate.Value); for (const TPair& FileChangeData : FileChanges) { if (!FileChangeData.Value || bIncludeDirs) { Changes.Add(FileChangeData.Key); } } Delegate.Key.Execute(Changes); } } FileChanges.Empty(); } } void FDirectoryWatchRequestLinux::WatchDirectoryTree(const FString & RootAbsolutePath, TArray>* 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 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(Ptr); Ptr += EVENT_SIZE + Event->len; // Skip if overflowed if ((Event->wd != -1) && (Event->mask & IN_Q_OVERFLOW) == 0) { TArray 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& 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