//===- DirectoryWatcher-windows.cpp - Windows-platform directory watching -===// // // 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 "DirectoryScanner.h" #include "clang/DirectoryWatcher/DirectoryWatcher.h" #include "llvm/ADT/STLExtras.h" #include "llvm/Support/ConvertUTF.h" #include "llvm/Support/Path.h" #include "llvm/Support/Windows/WindowsSupport.h" #include #include #include #include #include #include namespace { using DirectoryWatcherCallback = std::function, bool)>; using namespace llvm; using namespace clang; class DirectoryWatcherWindows : public clang::DirectoryWatcher { OVERLAPPED Overlapped; std::vector Notifications; std::thread WatcherThread; std::thread HandlerThread; std::function, bool)> Callback; SmallString Path; HANDLE Terminate; std::mutex Mutex; bool WatcherActive = false; std::condition_variable Ready; class EventQueue { std::mutex M; std::queue Q; std::condition_variable CV; public: void emplace(DirectoryWatcher::Event::EventKind Kind, StringRef Path) { { std::unique_lock L(M); Q.emplace(Kind, Path); } CV.notify_one(); } DirectoryWatcher::Event pop_front() { std::unique_lock L(M); while (true) { if (!Q.empty()) { DirectoryWatcher::Event E = Q.front(); Q.pop(); return E; } CV.wait(L, [this]() { return !Q.empty(); }); } } } Q; public: DirectoryWatcherWindows(HANDLE DirectoryHandle, bool WaitForInitialSync, DirectoryWatcherCallback Receiver); ~DirectoryWatcherWindows() override; void InitialScan(); void WatcherThreadProc(HANDLE DirectoryHandle); void NotifierThreadProc(bool WaitForInitialSync); }; DirectoryWatcherWindows::DirectoryWatcherWindows( HANDLE DirectoryHandle, bool WaitForInitialSync, DirectoryWatcherCallback Receiver) : Callback(Receiver), Terminate(INVALID_HANDLE_VALUE) { // Pre-compute the real location as we will be handing over the directory // handle to the watcher and performing synchronous operations. { DWORD Size = GetFinalPathNameByHandleW(DirectoryHandle, NULL, 0, 0); std::unique_ptr Buffer{new WCHAR[Size + 1]}; Size = GetFinalPathNameByHandleW(DirectoryHandle, Buffer.get(), Size, 0); Buffer[Size] = L'\0'; WCHAR *Data = Buffer.get(); if (Size >= 4 && ::memcmp(Data, L"\\\\?\\", 8) == 0) { Data += 4; Size -= 4; } llvm::sys::windows::UTF16ToUTF8(Data, Size, Path); } size_t EntrySize = sizeof(FILE_NOTIFY_INFORMATION) + MAX_PATH * sizeof(WCHAR); Notifications.resize((4 * EntrySize) / sizeof(DWORD)); memset(&Overlapped, 0, sizeof(Overlapped)); Overlapped.hEvent = CreateEventW(NULL, /*bManualReset=*/FALSE, /*bInitialState=*/FALSE, NULL); assert(Overlapped.hEvent && "unable to create event"); Terminate = CreateEventW(NULL, /*bManualReset=*/TRUE, /*bInitialState=*/FALSE, NULL); WatcherThread = std::thread([this, DirectoryHandle]() { this->WatcherThreadProc(DirectoryHandle); }); if (WaitForInitialSync) InitialScan(); HandlerThread = std::thread([this, WaitForInitialSync]() { this->NotifierThreadProc(WaitForInitialSync); }); } DirectoryWatcherWindows::~DirectoryWatcherWindows() { // Signal the Watcher to exit. SetEvent(Terminate); HandlerThread.join(); WatcherThread.join(); CloseHandle(Terminate); CloseHandle(Overlapped.hEvent); } void DirectoryWatcherWindows::InitialScan() { std::unique_lock lock(Mutex); Ready.wait(lock, [this] { return this->WatcherActive; }); Callback(getAsFileEvents(scanDirectory(Path.data())), /*IsInitial=*/true); } void DirectoryWatcherWindows::WatcherThreadProc(HANDLE DirectoryHandle) { while (true) { // We do not guarantee subdirectories, but macOS already provides // subdirectories, might as well as ... BOOL WatchSubtree = TRUE; DWORD NotifyFilter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION; DWORD BytesTransferred; if (!ReadDirectoryChangesW(DirectoryHandle, Notifications.data(), Notifications.size() * sizeof(DWORD), WatchSubtree, NotifyFilter, &BytesTransferred, &Overlapped, NULL)) { Q.emplace(DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""); break; } if (!WatcherActive) { std::unique_lock lock(Mutex); WatcherActive = true; } Ready.notify_one(); HANDLE Handles[2] = { Terminate, Overlapped.hEvent }; switch (WaitForMultipleObjects(2, Handles, FALSE, INFINITE)) { case WAIT_OBJECT_0: // Terminate Request case WAIT_FAILED: // Failure Q.emplace(DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""); (void)CloseHandle(DirectoryHandle); return; case WAIT_TIMEOUT: // Spurious wakeup? continue; case WAIT_OBJECT_0 + 1: // Directory change break; } if (!GetOverlappedResult(DirectoryHandle, &Overlapped, &BytesTransferred, FALSE)) { Q.emplace(DirectoryWatcher::Event::EventKind::WatchedDirRemoved, ""); Q.emplace(DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""); break; } // There was a buffer underrun on the kernel side. We may have lost // events, please re-synchronize. if (BytesTransferred == 0) { Q.emplace(DirectoryWatcher::Event::EventKind::WatcherGotInvalidated, ""); break; } for (FILE_NOTIFY_INFORMATION *I = (FILE_NOTIFY_INFORMATION *)Notifications.data(); I; I = I->NextEntryOffset ? (FILE_NOTIFY_INFORMATION *)((CHAR *)I + I->NextEntryOffset) : NULL) { DirectoryWatcher::Event::EventKind Kind = DirectoryWatcher::Event::EventKind::WatcherGotInvalidated; switch (I->Action) { case FILE_ACTION_ADDED: case FILE_ACTION_MODIFIED: case FILE_ACTION_RENAMED_NEW_NAME: Kind = DirectoryWatcher::Event::EventKind::Modified; break; case FILE_ACTION_REMOVED: case FILE_ACTION_RENAMED_OLD_NAME: Kind = DirectoryWatcher::Event::EventKind::Removed; break; } SmallString filename; sys::windows::UTF16ToUTF8(I->FileName, I->FileNameLength / sizeof(WCHAR), filename); Q.emplace(Kind, filename); } } (void)CloseHandle(DirectoryHandle); } void DirectoryWatcherWindows::NotifierThreadProc(bool WaitForInitialSync) { // If we did not wait for the initial sync, then we should perform the // scan when we enter the thread. if (!WaitForInitialSync) this->InitialScan(); while (true) { DirectoryWatcher::Event E = Q.pop_front(); Callback(E, /*IsInitial=*/false); if (E.Kind == DirectoryWatcher::Event::EventKind::WatcherGotInvalidated) break; } } auto error(DWORD ErrorCode) { DWORD Flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; LPSTR Buffer; if (!FormatMessageA(Flags, NULL, ErrorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&Buffer, 0, NULL)) { return make_error("error " + utostr(ErrorCode), inconvertibleErrorCode()); } std::string Message{Buffer}; LocalFree(Buffer); return make_error(Message, inconvertibleErrorCode()); } } // namespace llvm::Expected> clang::DirectoryWatcher::create(StringRef Path, DirectoryWatcherCallback Receiver, bool WaitForInitialSync) { if (Path.empty()) llvm::report_fatal_error( "DirectoryWatcher::create can not accept an empty Path."); if (!sys::fs::is_directory(Path)) llvm::report_fatal_error( "DirectoryWatcher::create can not accept a filepath."); SmallVector WidePath; if (sys::windows::UTF8ToUTF16(Path, WidePath)) return llvm::make_error( "unable to convert path to UTF-16", llvm::inconvertibleErrorCode()); DWORD DesiredAccess = FILE_LIST_DIRECTORY; DWORD ShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; DWORD CreationDisposition = OPEN_EXISTING; DWORD FlagsAndAttributes = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED; HANDLE DirectoryHandle = CreateFileW(WidePath.data(), DesiredAccess, ShareMode, /*lpSecurityAttributes=*/NULL, CreationDisposition, FlagsAndAttributes, NULL); if (DirectoryHandle == INVALID_HANDLE_VALUE) return error(GetLastError()); // NOTE: We use the watcher instance as a RAII object to discard the handles // for the directory in case of an error. Hence, this is early allocated, // with the state being written directly to the watcher. return std::make_unique( DirectoryHandle, WaitForInitialSync, Receiver); }