Index: clang/include/clang/DirectoryWatcher/DirectoryWatcher.h =================================================================== --- /dev/null +++ clang/include/clang/DirectoryWatcher/DirectoryWatcher.h @@ -0,0 +1,76 @@ +//===- DirectoryWatcher.h - Listens for directory file changes --*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// \file +/// \brief Utility class for listening for file system changes in a directory. +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_DIRECTORYWATCHER_DIRECTORYWATCHER_H +#define LLVM_CLANG_DIRECTORYWATCHER_DIRECTORYWATCHER_H + +#include "clang/Basic/LLVM.h" +#include "llvm/Support/Chrono.h" +#include +#include +#include + +namespace clang { + +/// Provides notifications for file system changes in a directory. +/// +/// Guarantees that the first time the directory is processed, the receiver will +/// be invoked even if the directory is empty. +class DirectoryWatcher { +public: + enum class EventKind { + /// A file was added to the directory. + /// + /// A file gets moved into the directory and replaces an existing file + /// with the same name will trigger an 'Added' event but no 'Removed' event. + /// If a file gets replaced multiple times within a short time period, it + /// may result in only one 'Added' event due to coalescing by the file + /// system notification mechanism. + Added, + /// A file was removed. + /// + /// A file that got replaced by another one with the same name will result + /// in a single 'Added' event, not a 'Removed' one. + Removed, + /// A file was modified. + Modified, + /// The watched directory got deleted. No more events will follow. + DirectoryDeleted, + }; + + struct Event { + EventKind Kind; + std::string Filename; + }; + + typedef std::function Events, bool isInitial)> + EventReceiver; + + ~DirectoryWatcher(); + + static std::unique_ptr create(StringRef Path, + EventReceiver Receiver, + bool waitInitialSync, + std::string &Error); + +private: + struct Implementation; + Implementation &Impl; + + DirectoryWatcher(); + + DirectoryWatcher(const DirectoryWatcher &) = delete; + DirectoryWatcher &operator=(const DirectoryWatcher &) = delete; +}; + +} // namespace clang + +#endif // LLVM_CLANG_DIRECTORYWATCHER_DIRECTORYWATCHER_H Index: clang/lib/CMakeLists.txt =================================================================== --- clang/lib/CMakeLists.txt +++ clang/lib/CMakeLists.txt @@ -18,6 +18,7 @@ add_subdirectory(Frontend) add_subdirectory(FrontendTool) add_subdirectory(Tooling) +add_subdirectory(DirectoryWatcher) add_subdirectory(Index) if(CLANG_ENABLE_STATIC_ANALYZER) add_subdirectory(StaticAnalyzer) Index: clang/lib/DirectoryWatcher/CMakeLists.txt =================================================================== --- /dev/null +++ clang/lib/DirectoryWatcher/CMakeLists.txt @@ -0,0 +1,34 @@ +include(CheckIncludeFiles) + +set(LLVM_LINK_COMPONENTS support) + +add_clang_library(clangDirectoryWatcher + DirectoryWatcher.cpp + ) + +if(APPLE) + check_include_files("CoreServices/CoreServices.h" HAVE_CORESERVICES) + if(HAVE_CORESERVICES) + set(DIRECTORY_WATCHER_FLAGS "${DIRECTORY_WATCHER_FLAGS} -framework CoreServices") + set_property(TARGET clangDirectoryWatcher APPEND_STRING PROPERTY + LINK_FLAGS ${DIRECTORY_WATCHER_FLAGS}) + endif() +elseif(CMAKE_SYSTEM_NAME MATCHES "Linux") + check_include_files("sys/inotify.h" HAVE_INOTIFY) +endif() + +llvm_canonicalize_cmake_booleans( + HAVE_CORESERVICES) +llvm_canonicalize_cmake_booleans( + HAVE_INOTIFY) + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/Config.inc.in + ${CMAKE_CURRENT_BINARY_DIR}/Config.inc +) + +if(BUILD_SHARED_LIBS AND APPLE AND HAVE_CORESERVICES) + set(DIRECTORY_WATCHER_FLAGS "${DIRECTORY_WATCHER_FLAGS} -framework CoreServices") + set_property(TARGET clangDirectoryWatcher APPEND_STRING PROPERTY + LINK_FLAGS ${DIRECTORY_WATCHER_FLAGS}) +endif() \ No newline at end of file Index: clang/lib/DirectoryWatcher/Config.inc.in =================================================================== --- /dev/null +++ clang/lib/DirectoryWatcher/Config.inc.in @@ -0,0 +1,2 @@ +#define HAVE_CORESERVICES @HAVE_CORESERVICES@ +#define HAVE_INOTIFY @HAVE_INOTIFY@ \ No newline at end of file Index: clang/lib/DirectoryWatcher/DirectoryWatcher-linux.inc.h =================================================================== --- /dev/null +++ clang/lib/DirectoryWatcher/DirectoryWatcher-linux.inc.h @@ -0,0 +1,186 @@ +//===- DirectoryWatcher-linux.inc.h - Linux-platform directory listening --===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/Errno.h" +#include "llvm/Support/Mutex.h" +#include "llvm/Support/Path.h" +#include +#include +#include + +namespace { + +struct INotifyEvent { + DirectoryWatcher::EventKind K; + std::string Filename; + Optional Status; +}; + +class EventQueue { + DirectoryWatcher::EventReceiver Receiver; + sys::Mutex Mtx; + bool gotInitialScan = false; + std::vector PendingEvents; + + DirectoryWatcher::Event toDirEvent(const INotifyEvent &evt) { + return DirectoryWatcher::Event{evt.K, evt.Filename}; + } + +public: + explicit EventQueue(DirectoryWatcher::EventReceiver receiver) + : Receiver(receiver) {} + + void onDirectoryEvents(ArrayRef evts) { + sys::ScopedLock L(Mtx); + + if (!gotInitialScan) { + PendingEvents.insert(PendingEvents.end(), evts.begin(), evts.end()); + return; + } + + SmallVector dirEvents; + for (const auto &evt : evts) { + dirEvents.push_back(toDirEvent(evt)); + } + Receiver(dirEvents, /*isInitial=*/false); + } + + void onInitialScan(std::shared_ptr dirScan) { + sys::ScopedLock L(Mtx); + + std::vector events = dirScan->getAsFileEvents(); + Receiver(events, /*isInitial=*/true); + + events.clear(); + for (const auto &evt : PendingEvents) { + if (evt.K == DirectoryWatcher::EventKind::Added && + dirScan->FileIDSet.count(evt.Status->getUniqueID())) { + // Already reported this event at the initial directory scan. + continue; + } + events.push_back(toDirEvent(evt)); + } + if (!events.empty()) { + Receiver(events, /*isInitial=*/false); + } + + gotInitialScan = true; + PendingEvents.clear(); + } +}; +} // namespace + +struct DirectoryWatcher::Implementation { + bool initialize(StringRef Path, EventReceiver Receiver, bool waitInitialSync, + std::string &Error); + ~Implementation() { stopListening(); }; + +private: + int inotifyFD = -1; + + void stopListening(); +}; + +static void runWatcher(std::string pathToWatch, int inotifyFD, + std::shared_ptr evtQueue) { + constexpr size_t EventBufferLength = + 30 * (sizeof(struct inotify_event) + NAME_MAX + 1); + char buf[EventBufferLength] __attribute__((aligned(8))); + + while (1) { + ssize_t numRead = llvm::sys::RetryAfterSignal( + -1, read, inotifyFD, reinterpret_cast(buf), EventBufferLength); + + SmallVector iEvents; + for (char *p = buf; p < buf + numRead;) { + assert(p + sizeof(struct inotify_event) <= buf + numRead && + "a whole inotify_event was read"); + struct inotify_event *ievt = reinterpret_cast(p); + p += sizeof(struct inotify_event) + ievt->len; + + if (ievt->mask & IN_DELETE_SELF) { + INotifyEvent iEvt{DirectoryWatcher::EventKind::DirectoryDeleted, + pathToWatch, None}; + iEvents.push_back(iEvt); + break; + } + + DirectoryWatcher::EventKind K = [&ievt]() { + if (ievt->mask & IN_MODIFY) + return DirectoryWatcher::EventKind::Modified; + if (ievt->mask & IN_MOVED_TO) + return DirectoryWatcher::EventKind::Added; + if (ievt->mask & IN_DELETE) + return DirectoryWatcher::EventKind::Removed; + llvm_unreachable("Unknown event type."); + }(); + + assert(ievt->len > 0 && "expected a filename from inotify"); + SmallString<256> fullPath{pathToWatch}; + sys::path::append(fullPath, ievt->name); + + Optional statusOpt; + INotifyEvent iEvt{K, fullPath.str(), statusOpt}; + iEvents.push_back(iEvt); + } + + if (!iEvents.empty()) + evtQueue->onDirectoryEvents(iEvents); + } +} + +bool DirectoryWatcher::Implementation::initialize(StringRef Path, + EventReceiver Receiver, + bool waitInitialSync, + std::string &errorMsg) { + auto error = [&](StringRef msg) -> bool { + errorMsg = msg; + errorMsg += ": "; + errorMsg += llvm::sys::StrError(); + return true; + }; + + auto evtQueue = std::make_shared(std::move(Receiver)); + + inotifyFD = inotify_init(); + if (inotifyFD == -1) + return error("inotify_init failed"); + + std::string pathToWatch = Path; + int wd = inotify_add_watch(inotifyFD, pathToWatch.c_str(), + IN_MOVED_TO | IN_DELETE | IN_MODIFY | + IN_DELETE_SELF | IN_ONLYDIR); + if (wd == -1) + return error("inotify_add_watch failed"); + + std::thread watchThread( + std::bind(runWatcher, pathToWatch, inotifyFD, evtQueue)); + watchThread.detach(); + + auto initialScan = std::make_shared(); + auto runScan = [pathToWatch, initialScan, evtQueue]() { + initialScan->scanDirectory(pathToWatch); + evtQueue->onInitialScan(std::move(initialScan)); + }; + + if (waitInitialSync) { + runScan(); + } else { + std::thread scanThread(runScan); + scanThread.detach(); + } + + return false; +} + +void DirectoryWatcher::Implementation::stopListening() { + if (inotifyFD == -1) + return; + llvm::sys::RetryAfterSignal(-1, close, inotifyFD); + inotifyFD = -1; +} Index: clang/lib/DirectoryWatcher/DirectoryWatcher-mac.inc.h =================================================================== --- /dev/null +++ clang/lib/DirectoryWatcher/DirectoryWatcher-mac.inc.h @@ -0,0 +1,209 @@ +//===- DirectoryWatcher-mac.inc.h - Mac-platform directory listening ------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include + +struct DirectoryWatcher::Implementation { + bool initialize(StringRef Path, EventReceiver Receiver, bool waitInitialSync, + std::string &Error); + ~Implementation() { stopFSEventStream(); }; + +private: + FSEventStreamRef EventStream = nullptr; + + bool setupFSEventStream(StringRef path, EventReceiver receiver, + dispatch_queue_t queue, + std::shared_ptr initialScanPtr); + void stopFSEventStream(); +}; + +namespace { +struct EventStreamContextData { + std::string WatchedPath; + DirectoryWatcher::EventReceiver Receiver; + std::shared_ptr InitialScan; + + EventStreamContextData(std::string watchedPath, + DirectoryWatcher::EventReceiver receiver, + std::shared_ptr initialScanPtr) + : WatchedPath(std::move(watchedPath)), Receiver(std::move(receiver)), + InitialScan(std::move(initialScanPtr)) {} + + static void dispose(const void *ctx) { + delete static_cast(ctx); + } +}; +} // namespace + +static void eventStreamCallback(ConstFSEventStreamRef stream, + void *clientCallBackInfo, size_t numEvents, + void *eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[]) { + auto *ctx = static_cast(clientCallBackInfo); + + std::vector Events; + for (size_t i = 0; i < numEvents; ++i) { + StringRef path = ((const char **)eventPaths)[i]; + const FSEventStreamEventFlags flags = eventFlags[i]; + if (!(flags & kFSEventStreamEventFlagItemIsFile)) { + if ((flags & kFSEventStreamEventFlagItemRemoved) && + path == ctx->WatchedPath) { + DirectoryWatcher::Event Evt{ + DirectoryWatcher::EventKind::DirectoryDeleted, path}; + Events.push_back(Evt); + break; + } + continue; + } + DirectoryWatcher::EventKind K = DirectoryWatcher::EventKind::Modified; + bool hasAddedFlag = flags & (kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemRenamed); + bool hasRemovedFlag = flags & kFSEventStreamEventFlagItemRemoved; + Optional statusOpt; + // NOTE: With low latency sometimes for a file that is moved inside the + // directory, or for a file that is removed from the directory, the flags + // have both 'renamed' and 'removed'. We use getting the file status as a + // way to distinguish between the two. + if (hasAddedFlag) { + statusOpt = getFileStatus(path); + if (statusOpt.hasValue()) { + K = DirectoryWatcher::EventKind::Added; + } else { + K = DirectoryWatcher::EventKind::Removed; + } + } else if (hasRemovedFlag) { + K = DirectoryWatcher::EventKind::Removed; + } else { + statusOpt = getFileStatus(path); + if (!statusOpt.hasValue()) { + K = DirectoryWatcher::EventKind::Removed; + } + } + + if (ctx->InitialScan && K == DirectoryWatcher::EventKind::Added) { + // For the first time we get the events, check that we haven't already + // sent the 'added' event at the initial scan. + if (ctx->InitialScan->FileIDSet.count(statusOpt->getUniqueID())) { + // Already reported this event at the initial directory scan. + continue; + } + } + + DirectoryWatcher::Event Evt{K, path}; + Events.push_back(Evt); + } + + // We won't need to check again later on. + ctx->InitialScan.reset(); + + if (!Events.empty()) { + ctx->Receiver(Events, /*isInitial=*/false); + } +} + +bool DirectoryWatcher::Implementation::setupFSEventStream( + StringRef path, EventReceiver receiver, dispatch_queue_t queue, + std::shared_ptr initialScanPtr) { + if (path.empty()) + return true; + + CFMutableArrayRef pathsToWatch = + CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks); + CFStringRef cfPathStr = + CFStringCreateWithBytes(nullptr, (const UInt8 *)path.data(), path.size(), + kCFStringEncodingUTF8, false); + CFArrayAppendValue(pathsToWatch, cfPathStr); + CFRelease(cfPathStr); + CFAbsoluteTime latency = 0.0; // Latency in seconds. + + std::string realPath; + { + SmallString<128> Storage; + StringRef P = llvm::Twine(path).toNullTerminatedStringRef(Storage); + char Buffer[PATH_MAX]; + // Use ::realpath to get the real path name + if (::realpath(P.begin(), Buffer) != nullptr) + realPath = Buffer; + else + realPath = path; + } + + EventStreamContextData *ctxData = new EventStreamContextData( + std::move(realPath), std::move(receiver), std::move(initialScanPtr)); + FSEventStreamContext context; + context.version = 0; + context.info = ctxData; + context.retain = nullptr; + context.release = EventStreamContextData::dispose; + context.copyDescription = nullptr; + + EventStream = FSEventStreamCreate( + nullptr, eventStreamCallback, &context, pathsToWatch, + kFSEventStreamEventIdSinceNow, latency, + kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer); + CFRelease(pathsToWatch); + if (!EventStream) { + return true; + } + FSEventStreamSetDispatchQueue(EventStream, queue); + FSEventStreamStart(EventStream); + return false; +} + +void DirectoryWatcher::Implementation::stopFSEventStream() { + if (!EventStream) + return; + FSEventStreamStop(EventStream); + FSEventStreamInvalidate(EventStream); + FSEventStreamRelease(EventStream); + EventStream = nullptr; +} + +bool DirectoryWatcher::Implementation::initialize(StringRef Path, + EventReceiver Receiver, + bool waitInitialSync, + std::string &Error) { + auto initialScan = std::make_shared(); + + dispatch_queue_t queue = + dispatch_queue_create("DirectoryWatcher", DISPATCH_QUEUE_SERIAL); + dispatch_semaphore_t initScanSema = dispatch_semaphore_create(0); + dispatch_semaphore_t setupFSEventsSema = dispatch_semaphore_create(0); + + std::string copiedPath = Path; + dispatch_retain(initScanSema); + dispatch_retain(setupFSEventsSema); + dispatch_async(queue, ^{ + // Wait for the event stream to be setup before doing the initial scan, + // to make sure we won't miss any events. + dispatch_semaphore_wait(setupFSEventsSema, DISPATCH_TIME_FOREVER); + initialScan->scanDirectory(copiedPath); + Receiver(initialScan->getAsFileEvents(), /*isInitial=*/true); + dispatch_semaphore_signal(initScanSema); + dispatch_release(setupFSEventsSema); + dispatch_release(initScanSema); + }); + bool fsErr = setupFSEventStream(Path, Receiver, queue, initialScan); + dispatch_semaphore_signal(setupFSEventsSema); + + if (waitInitialSync) { + dispatch_semaphore_wait(initScanSema, DISPATCH_TIME_FOREVER); + } + dispatch_release(setupFSEventsSema); + dispatch_release(initScanSema); + dispatch_release(queue); + + if (fsErr) { + raw_string_ostream(Error) + << "failed to setup FSEvents stream for path: " << Path; + return true; + } + + return false; +} Index: clang/lib/DirectoryWatcher/DirectoryWatcher.cpp =================================================================== --- /dev/null +++ clang/lib/DirectoryWatcher/DirectoryWatcher.cpp @@ -0,0 +1,149 @@ +//===- DirectoryWatcher.cpp - Listens for directory file changes ----------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// \file +/// \brief Utility class for listening for file system changes in a directory. +//===----------------------------------------------------------------------===// + +#include "clang/DirectoryWatcher/DirectoryWatcher.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang; +using namespace llvm; + +static Optional getFileStatus(StringRef path) { + sys::fs::file_status Status; + std::error_code EC = status(path, Status); + if (EC) + return None; + return Status; +} + +namespace llvm { +// Specialize DenseMapInfo for sys::fs::UniqueID. +template <> struct DenseMapInfo { + static sys::fs::UniqueID getEmptyKey() { + return sys::fs::UniqueID{DenseMapInfo::getEmptyKey(), + DenseMapInfo::getEmptyKey()}; + } + + static sys::fs::UniqueID getTombstoneKey() { + return sys::fs::UniqueID{DenseMapInfo::getTombstoneKey(), + DenseMapInfo::getEmptyKey()}; + } + + static unsigned getHashValue(const sys::fs::UniqueID &val) { + return DenseMapInfo>::getHashValue( + std::make_pair(val.getDevice(), val.getFile())); + } + + static bool isEqual(const sys::fs::UniqueID &LHS, + const sys::fs::UniqueID &RHS) { + return LHS == RHS; + } +}; +} // namespace llvm + +namespace { +/// Used for initial directory scan. +/// +/// Note that the caller must ensure serial access to it. It is not thread safe +/// to access it without additional protection. +struct DirectoryScan { + DenseSet FileIDSet; + std::vector>> Files; + + void scanDirectory(StringRef Path) { + using namespace llvm::sys; + + std::error_code EC; + for (auto It = fs::directory_iterator(Path, EC), + End = fs::directory_iterator(); + !EC && It != End; It.increment(EC)) { + auto status = getFileStatus(It->path()); + if (!status.hasValue()) + continue; + Files.push_back( + std::make_tuple(It->path(), status->getLastModificationTime())); + FileIDSet.insert(status->getUniqueID()); + } + } + + std::vector getAsFileEvents() const { + std::vector Events; + for (const auto &info : Files) { + DirectoryWatcher::Event Event{DirectoryWatcher::EventKind::Added, + std::get<0>(info)}; + Events.push_back(std::move(Event)); + } + return Events; + } +}; +} // namespace + +// Add platform-specific functionality. +#include "Config.inc" + +#if HAVE_CORESERVICES +#include "DirectoryWatcher-mac.inc.h" +#elif HAVE_INOTIFY +#include "DirectoryWatcher-linux.inc.h" +#else + +struct DirectoryWatcher::Implementation { + bool initialize(StringRef Path, EventReceiver Receiver, bool waitInitialSync, + std::string &Error) { + Error = "directory listening not supported for this platform"; + return true; + } +}; + +#endif + +DirectoryWatcher::DirectoryWatcher() : Impl(*new Implementation()) {} + +DirectoryWatcher::~DirectoryWatcher() { delete &Impl; } + +std::unique_ptr +DirectoryWatcher::create(StringRef Path, EventReceiver Receiver, + bool waitInitialSync, std::string &Error) { + using namespace llvm::sys; + + if (!fs::exists(Path)) { + std::error_code EC = fs::create_directories(Path); + if (EC) { + Error = EC.message(); + return nullptr; + } + } + + bool IsDir; + std::error_code EC = fs::is_directory(Path, IsDir); + if (EC) { + Error = EC.message(); + return nullptr; + } + if (!IsDir) { + Error = "path is not a directory: "; + Error += Path; + return nullptr; + } + + std::unique_ptr DirWatch; + DirWatch.reset(new DirectoryWatcher()); + auto &Impl = DirWatch->Impl; + bool hasError = + Impl.initialize(Path, std::move(Receiver), waitInitialSync, Error); + if (hasError) + return nullptr; + + return DirWatch; +} Index: clang/unittests/CMakeLists.txt =================================================================== --- clang/unittests/CMakeLists.txt +++ clang/unittests/CMakeLists.txt @@ -30,5 +30,6 @@ if(NOT WIN32 AND CLANG_TOOL_LIBCLANG_BUILD) add_subdirectory(libclang) endif() +add_subdirectory(DirectoryWatcher) add_subdirectory(Rename) add_subdirectory(Index) Index: clang/unittests/DirectoryWatcher/CMakeLists.txt =================================================================== --- /dev/null +++ clang/unittests/DirectoryWatcher/CMakeLists.txt @@ -0,0 +1,22 @@ +set(LLVM_LINK_COMPONENTS + Support + ) + +add_clang_unittest(DirectoryWatcherTests + DirectoryWatcherTest.cpp + ) + +target_link_libraries(DirectoryWatcherTests + PRIVATE + clangDirectoryWatcher + clangBasic + ) + +if(APPLE) + check_include_files("CoreServices/CoreServices.h" HAVE_CORESERVICES_H) + if(HAVE_CORESERVICES_H) + set(DWT_LINK_FLAGS "${DWT_LINK_FLAGS} -framework CoreServices") + set_property(TARGET DirectoryWatcherTests APPEND_STRING PROPERTY + LINK_FLAGS ${DWT_LINK_FLAGS}) + endif() +endif() Index: clang/unittests/DirectoryWatcher/DirectoryWatcherTest.cpp =================================================================== --- /dev/null +++ clang/unittests/DirectoryWatcher/DirectoryWatcherTest.cpp @@ -0,0 +1,327 @@ +//===- unittests/DirectoryWatcher/DirectoryWatcherTest.cpp ----------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/DirectoryWatcher/DirectoryWatcher.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Path.h" +#include "gtest/gtest.h" +#include +#include +#include + +using namespace llvm; +using namespace llvm::sys; +using namespace llvm::sys::fs; +using namespace clang; + +namespace { + +class EventCollection { + SmallVector Events; + +public: + EventCollection() = default; + explicit EventCollection(ArrayRef events) { + append(events); + } + + void append(ArrayRef events) { + Events.append(events.begin(), events.end()); + } + + bool empty() const { return Events.empty(); } + size_t size() const { return Events.size(); } + void clear() { Events.clear(); } + + bool hasEvents(ArrayRef filenames, + ArrayRef kinds, + ArrayRef stats) const { + assert(filenames.size() == kinds.size()); + assert(filenames.size() == stats.size()); + SmallVector evts = Events; + bool hadError = false; + for (unsigned i = 0, e = filenames.size(); i < e; ++i) { + StringRef fname = filenames[i]; + DirectoryWatcher::EventKind kind = kinds[i]; + auto it = std::find_if(evts.begin(), evts.end(), + [&](const DirectoryWatcher::Event &evt) -> bool { + return path::filename(evt.Filename) == fname; + }); + if (it == evts.end()) { + hadError = err(Twine("expected filename '" + fname + "' not found")); + continue; + } + if (it->Kind != kind) { + hadError = err(Twine("filename '" + fname + "' has event kind " + + std::to_string((int)it->Kind) + ", expected ") + + std::to_string((int)kind)); + } + evts.erase(it); + } + for (const auto &evt : evts) { + hadError = err(Twine("unexpected filename '" + + path::filename(evt.Filename) + "' found")); + } + return !hadError; + } + + bool hasAdded(ArrayRef filenames, + ArrayRef stats) const { + std::vector kinds{ + filenames.size(), DirectoryWatcher::EventKind::Added}; + return hasEvents(filenames, kinds, stats); + } + + bool hasRemoved(ArrayRef filenames) const { + std::vector kinds{ + filenames.size(), DirectoryWatcher::EventKind::Removed}; + std::vector stats{filenames.size(), file_status{}}; + return hasEvents(filenames, kinds, stats); + } + +private: + bool err(Twine msg) const { + SmallString<128> buf; + llvm::errs() << msg.toStringRef(buf) << '\n'; + return true; + } +}; + +struct EventOccurrence { + std::vector Events; + bool IsInitial; +}; + +class DirectoryWatcherTest + : public std::enable_shared_from_this { + std::string WatchedDir; + std::string TempDir; + std::unique_ptr DirWatcher; + + std::condition_variable Condition; + std::mutex Mutex; + std::deque EvtOccurs; + +public: + void init() { + SmallString<128> pathBuf; + std::error_code EC = createUniqueDirectory("dirwatcher", pathBuf); + ASSERT_FALSE(EC); + TempDir = pathBuf.str(); + path::append(pathBuf, "watch"); + WatchedDir = pathBuf.str(); + EC = create_directory(WatchedDir); + ASSERT_FALSE(EC); + } + + ~DirectoryWatcherTest() { + stopWatching(); + remove_directories(TempDir); + } + +public: + StringRef getWatchedDir() const { return WatchedDir; } + + void addFile(StringRef filename, file_status &stat) { + SmallString<128> pathBuf; + pathBuf = TempDir; + path::append(pathBuf, filename); + Expected ft = + openNativeFileForWrite(pathBuf, CD_CreateNew, OF_None); + ASSERT_TRUE((bool)ft); + closeFile(*ft); + + SmallString<128> newPath; + newPath = WatchedDir; + path::append(newPath, filename); + std::error_code EC = rename(pathBuf, newPath); + ASSERT_FALSE(EC); + + EC = status(newPath, stat); + ASSERT_FALSE(EC); + } + + void addFiles(ArrayRef filenames, + std::vector &stats) { + for (auto fname : filenames) { + file_status stat; + addFile(fname, stat); + stats.push_back(stat); + } + } + + void addFiles(ArrayRef filenames) { + std::vector stats; + addFiles(filenames, stats); + } + + void removeFile(StringRef filename) { + SmallString<128> pathBuf; + pathBuf = WatchedDir; + path::append(pathBuf, filename); + std::error_code EC = remove(pathBuf, /*IgnoreNonExisting=*/false); + ASSERT_FALSE(EC); + } + + void removeFiles(ArrayRef filenames) { + for (auto fname : filenames) { + removeFile(fname); + } + } + + /// \returns true for error. + bool startWatching(bool waitInitialSync) { + std::weak_ptr weakThis = shared_from_this(); + auto receiver = [weakThis](ArrayRef events, + bool isInitial) { + if (auto this_ = weakThis.lock()) + this_->onEvents(events, isInitial); + }; + std::string error; + DirWatcher = DirectoryWatcher::create(getWatchedDir(), receiver, + waitInitialSync, error); + return DirWatcher == nullptr; + } + + void stopWatching() { DirWatcher.reset(); } + + /// \returns None if the timeout is reached before getting an event. + Optional getNextEvent(unsigned timeout_seconds = 5) { + std::unique_lock lck(Mutex); + auto pred = [&]() -> bool { return !EvtOccurs.empty(); }; + bool gotEvent = + Condition.wait_for(lck, std::chrono::seconds(timeout_seconds), pred); + if (!gotEvent) + return None; + + EventOccurrence occur = EvtOccurs.front(); + EvtOccurs.pop_front(); + return occur; + } + + EventOccurrence getNextEventImmediately() { + std::lock_guard LG(Mutex); + assert(!EvtOccurs.empty()); + EventOccurrence occur = EvtOccurs.front(); + EvtOccurs.pop_front(); + return occur; + } + +private: + void onEvents(ArrayRef events, bool isInitial) { + std::lock_guard LG(Mutex); + EvtOccurs.push_back({events, isInitial}); + Condition.notify_all(); + } +}; + +} // namespace + +TEST(DirectoryWatcherTest, initialScan) { + auto t = std::make_shared(); + t->init(); + + std::vector fnames = {"a", "b", "c"}; + std::vector stats; + t->addFiles(fnames, stats); + + bool err = t->startWatching(/*waitInitialSync=*/true); + ASSERT_FALSE(err); + + auto evt = t->getNextEventImmediately(); + EXPECT_TRUE(evt.IsInitial); + EventCollection coll1{evt.Events}; + EXPECT_TRUE(coll1.hasAdded(fnames, stats)); + + StringRef additionalFname = "d"; + file_status additionalStat; + t->addFile(additionalFname, additionalStat); + auto evtOpt = t->getNextEvent(); + ASSERT_TRUE(evtOpt.hasValue()); + EXPECT_FALSE(evtOpt->IsInitial); + EventCollection coll2{evtOpt->Events}; + EXPECT_TRUE(coll2.hasAdded({additionalFname}, {additionalStat})); +} + +TEST(DirectoryWatcherTest, fileEvents) { + auto t = std::make_shared(); + t->init(); + + bool err = t->startWatching(/*waitInitialSync=*/false); + ASSERT_FALSE(err); + + auto evt = t->getNextEvent(); + ASSERT_TRUE(evt.hasValue()); + EXPECT_TRUE(evt->IsInitial); + EXPECT_TRUE(evt->Events.empty()); + return; + + { + std::vector fnames = {"a", "b"}; + std::vector stats; + t->addFiles(fnames, stats); + + EventCollection coll{}; + while (coll.size() < 2) { + evt = t->getNextEvent(); + ASSERT_TRUE(evt.hasValue()); + coll.append(evt->Events); + } + EXPECT_TRUE(coll.hasAdded(fnames, stats)); + } + { + std::vector fnames = {"b", "c"}; + std::vector stats; + t->addFiles(fnames, stats); + + EventCollection coll{}; + while (coll.size() < 2) { + evt = t->getNextEvent(); + ASSERT_TRUE(evt.hasValue()); + coll.append(evt->Events); + } + EXPECT_TRUE(coll.hasAdded(fnames, stats)); + } + { + std::vector fnames = {"a", "c"}; + std::vector stats; + t->addFiles(fnames, stats); + t->removeFile("b"); + + EventCollection coll{}; + while (coll.size() < 3) { + evt = t->getNextEvent(); + ASSERT_TRUE(evt.hasValue()); + coll.append(evt->Events); + } + + EXPECT_TRUE(coll.hasEvents(std::vector{"a", "b", "c"}, + std::vector{ + DirectoryWatcher::EventKind::Added, + DirectoryWatcher::EventKind::Removed, + DirectoryWatcher::EventKind::Added, + }, + std::vector{ + stats[0], + file_status{}, + stats[1], + })); + } + { + std::vector fnames = {"a", "c"}; + t->removeFiles(fnames); + + EventCollection coll{}; + while (coll.size() < 2) { + evt = t->getNextEvent(); + ASSERT_TRUE(evt.hasValue()); + coll.append(evt->Events); + } + EXPECT_TRUE(coll.hasRemoved(fnames)); + } +}