// Copyright 2011 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "base/files/file_path_watcher.h" #include #include #include #include #include #include #include #include "base/auto_reset.h" #include "base/containers/heap_array.h" #include "base/containers/span.h" #include "base/files/file.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/logging.h" #include "base/memory/ptr_util.h" #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" #include "base/no_destructor.h" #include "base/strings/string_util.h" #include "base/synchronization/lock.h" #include "base/task/sequenced_task_runner.h" #include "base/threading/platform_thread.h" #include "base/threading/scoped_blocking_call.h" #include "base/time/time.h" #include "base/types/expected.h" #include "base/types/id_type.h" #include "base/win/object_watcher.h" #include "base/win/scoped_handle.h" #include "base/win/windows_types.h" namespace base { namespace { enum class CreateFileHandleError { // When watching a path, the path (or some of its ancestor directories) might // not exist yet. Failure to create a watcher because the path doesn't exist // (or is not a directory) should not be considered fatal, since the watcher // implementation can simply try again one directory level above. kNonFatal, kFatal, }; base::expected CreateDirectoryHandle(const FilePath& dir) { ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK); base::win::ScopedHandle handle(::CreateFileW( dir.value().c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr)); if (handle.is_valid()) { File::Info file_info; if (!GetFileInfo(dir, &file_info)) { // Windows sometimes hands out handles to files that are about to go away. return base::unexpected(CreateFileHandleError::kNonFatal); } // Only return the handle if its a directory. if (!file_info.is_directory) { return base::unexpected(CreateFileHandleError::kNonFatal); } return handle; } switch (::GetLastError()) { case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_ACCESS_DENIED: case ERROR_SHARING_VIOLATION: case ERROR_DIRECTORY: // Failure to create the handle is ok if the target directory doesn't // exist, access is denied (happens if the file is already gone but there // are still handles open), or the target is not a directory. return base::unexpected(CreateFileHandleError::kNonFatal); default: DPLOG(ERROR) << "CreateFileW failed for " << dir.value(); return base::unexpected(CreateFileHandleError::kFatal); } } class FilePathWatcherImpl; class CompletionIOPortThread final : public PlatformThread::Delegate { public: using WatcherEntryId = base::IdTypeU64; CompletionIOPortThread(const CompletionIOPortThread&) = delete; CompletionIOPortThread& operator=(const CompletionIOPortThread&) = delete; static CompletionIOPortThread* Get() { static NoDestructor io_thread; return io_thread.get(); } // Thread safe. std::optional AddWatcher( FilePathWatcherImpl& watcher, base::win::ScopedHandle watched_handle, base::FilePath watched_path); // Thread safe. void RemoveWatcher(WatcherEntryId watcher_id); Lock& GetLockForTest(); // IN-TEST private: friend NoDestructor; // The max size of a file notification assuming that long paths aren't // enabled. static constexpr size_t kMaxFileNotifySize = sizeof(FILE_NOTIFY_INFORMATION) + MAX_PATH; // Choose a decent number of notifications to support that isn't too large. // Whatever we choose will be doubled by the kernel's copy of the buffer. static constexpr int kBufferNotificationCount = 20; static constexpr size_t kWatchBufferSizeBytes = kBufferNotificationCount * kMaxFileNotifySize; // Must be DWORD aligned. static_assert(kWatchBufferSizeBytes % sizeof(DWORD) == 0); // Must be less than the max network packet size for network drives. See // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks. static_assert(kWatchBufferSizeBytes <= 64 * 1024); struct WatcherEntry { WatcherEntry(base::WeakPtr watcher_weak_ptr, scoped_refptr task_runner, base::win::ScopedHandle watched_handle, base::FilePath watched_path) : watcher_weak_ptr(std::move(watcher_weak_ptr)), task_runner(std::move(task_runner)), watched_handle(std::move(watched_handle)), watched_path(std::move(watched_path)) {} ~WatcherEntry() = default; // Delete copy and move constructors since `buffer` should not be copied or // moved. WatcherEntry(const WatcherEntry&) = delete; WatcherEntry& operator=(const WatcherEntry&) = delete; WatcherEntry(WatcherEntry&&) = delete; WatcherEntry& operator=(WatcherEntry&&) = delete; base::WeakPtr watcher_weak_ptr; scoped_refptr task_runner; base::win::ScopedHandle watched_handle; base::FilePath watched_path; alignas(DWORD) uint8_t buffer[kWatchBufferSizeBytes]; }; OVERLAPPED overlapped = {}; CompletionIOPortThread(); ~CompletionIOPortThread() override = default; void ThreadMain() override; [[nodiscard]] DWORD SetupWatch(WatcherEntry& watcher_entry); Lock watchers_lock_; WatcherEntryId::Generator watcher_id_generator_ GUARDED_BY(watchers_lock_); std::map watcher_entries_ GUARDED_BY(watchers_lock_); // It is safe to access `io_completion_port_` on any thread without locks // since: // - Windows Handles are thread safe // - `io_completion_port_` is set once in the constructor of this class // - This class is never destroyed. win::ScopedHandle io_completion_port_{ ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, reinterpret_cast(nullptr), 1)}; }; class FilePathWatcherImpl : public FilePathWatcher::PlatformDelegate { public: FilePathWatcherImpl() = default; FilePathWatcherImpl(const FilePathWatcherImpl&) = delete; FilePathWatcherImpl& operator=(const FilePathWatcherImpl&) = delete; ~FilePathWatcherImpl() override; // FilePathWatcher::PlatformDelegate implementation: bool Watch(const FilePath& path, Type type, const FilePathWatcher::Callback& callback) override; // FilePathWatcher::PlatformDelegate implementation: bool WatchWithOptions(const FilePath& path, const WatchOptions& flags, const FilePathWatcher::Callback& callback) override; // FilePathWatcher::PlatformDelegate implementation: bool WatchWithChangeInfo( const FilePath& path, const WatchOptions& options, const FilePathWatcher::CallbackWithChangeInfo& callback) override; void Cancel() override; Lock& GetWatchThreadLockForTest() override; // IN-TEST private: friend CompletionIOPortThread; // Sets up a watch handle for either `target_` or one of its ancestors. // Returns true on success. [[nodiscard]] bool SetupWatchHandleForTarget(); void CloseWatchHandle(); void BufferOverflowed(); void WatchedDirectoryDeleted(base::FilePath watched_path, base::HeapArray notification_batch); void ProcessNotificationBatch(base::FilePath watched_path, base::HeapArray notification_batch); // Callback to notify upon changes. FilePathWatcher::CallbackWithChangeInfo callback_; // Path we're supposed to watch (passed to callback). FilePath target_; std::optional watcher_id_; // The type of watch requested. Type type_ = Type::kNonRecursive; bool target_exists_ = false; WeakPtrFactory weak_factory_{this}; }; CompletionIOPortThread::CompletionIOPortThread() { PlatformThread::CreateNonJoinable(0, this); } DWORD CompletionIOPortThread::SetupWatch(WatcherEntry& watcher_entry) { bool success = ReadDirectoryChangesW( watcher_entry.watched_handle.get(), &watcher_entry.buffer, kWatchBufferSizeBytes, /*bWatchSubtree=*/true, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY, nullptr, &overlapped, nullptr); if (!success) { return ::GetLastError(); } return ERROR_SUCCESS; } std::optional CompletionIOPortThread::AddWatcher(FilePathWatcherImpl& watcher, base::win::ScopedHandle watched_handle, base::FilePath watched_path) { AutoLock auto_lock(watchers_lock_); WatcherEntryId watcher_id = watcher_id_generator_.GenerateNextId(); HANDLE port = ::CreateIoCompletionPort( watched_handle.get(), io_completion_port_.get(), static_cast(watcher_id.GetUnsafeValue()), 1); if (port == nullptr) { return std::nullopt; } auto [it, inserted] = watcher_entries_.emplace( std::piecewise_construct, std::forward_as_tuple(watcher_id), std::forward_as_tuple(watcher.weak_factory_.GetWeakPtr(), watcher.task_runner(), std::move(watched_handle), std::move(watched_path))); CHECK(inserted); DWORD result = SetupWatch(it->second); if (result != ERROR_SUCCESS) { watcher_entries_.erase(it); return std::nullopt; } return watcher_id; } void CompletionIOPortThread::RemoveWatcher(WatcherEntryId watcher_id) { HANDLE raw_watched_handle; { AutoLock auto_lock(watchers_lock_); auto it = watcher_entries_.find(watcher_id); CHECK(it != watcher_entries_.end()); auto& watched_handle = it->second.watched_handle; CHECK(watched_handle.is_valid()); raw_watched_handle = watched_handle.release(); } { ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK); // `raw_watched_handle` being closed indicates to `ThreadMain` that this // entry needs to be removed from `watcher_entries_` once the kernel // indicates it is safe too. ::CloseHandle(raw_watched_handle); } } Lock& CompletionIOPortThread::GetLockForTest() { return watchers_lock_; } void CompletionIOPortThread::ThreadMain() { while (true) { DWORD bytes_transferred; ULONG_PTR key = reinterpret_cast(nullptr); OVERLAPPED* overlapped_out = nullptr; BOOL io_port_result = ::GetQueuedCompletionStatus( io_completion_port_.get(), &bytes_transferred, &key, &overlapped_out, INFINITE); CHECK(&overlapped == overlapped_out); DWORD io_port_error = ERROR_SUCCESS; if (io_port_result == FALSE) { io_port_error = ::GetLastError(); // `ERROR_ACCESS_DENIED` should be the only error we can receive. CHECK_EQ(io_port_error, static_cast(ERROR_ACCESS_DENIED)); } AutoLock auto_lock(watchers_lock_); WatcherEntryId watcher_id = WatcherEntryId::FromUnsafeValue(key); auto watcher_entry_it = watcher_entries_.find(watcher_id); CHECK(watcher_entry_it != watcher_entries_.end()) << "WatcherEntryId not in map"; auto& watcher_entry = watcher_entry_it->second; auto& [watcher_weak_ptr, task_runner, watched_handle, watched_path, buffer] = watcher_entry; if (!watched_handle.is_valid()) { // After the handle has been closed, a final notification will be sent // with `bytes_transferred` equal to 0. It is safe to destroy the watcher // now. if (bytes_transferred == 0) { // `watcher_entry` and all the local refs to its members will be // dangling after this call. watcher_entries_.erase(watcher_entry_it); } continue; } // `GetQueuedCompletionStatus` can fail with `ERROR_ACCESS_DENIED` when the // watched directory is deleted. if (io_port_result == FALSE) { CHECK(bytes_transferred == 0); task_runner->PostTask( FROM_HERE, base::BindOnce(&FilePathWatcherImpl::WatchedDirectoryDeleted, watcher_weak_ptr, watched_path, base::HeapArray())); continue; } base::HeapArray notification_batch; if (bytes_transferred > 0) { notification_batch = base::HeapArray::CopiedFrom( base::span(buffer).first(bytes_transferred)); } // Let the kernel know that we're ready to receive change events again in // the `watcher_entry`'s `buffer`. // // We do this as soon as possible, so that not too many events are received // in the next batch. Too many events can cause a buffer overflow. DWORD result = SetupWatch(watcher_entry); // `SetupWatch` can fail if the watched directory was deleted before // `SetupWatch` was called but after `GetQueuedCompletionStatus` returned. if (result != ERROR_SUCCESS) { CHECK_EQ(result, static_cast(ERROR_ACCESS_DENIED)); task_runner->PostTask( FROM_HERE, base::BindOnce(&FilePathWatcherImpl::WatchedDirectoryDeleted, watcher_weak_ptr, watched_path, std::move(notification_batch))); continue; } // `GetQueuedCompletionStatus` succeeds with zero bytes transferred if there // is a buffer overflow. if (bytes_transferred == 0) { task_runner->PostTask( FROM_HERE, base::BindOnce(&FilePathWatcherImpl::BufferOverflowed, watcher_weak_ptr)); continue; } task_runner->PostTask( FROM_HERE, base::BindOnce(&FilePathWatcherImpl::ProcessNotificationBatch, watcher_weak_ptr, watched_path, std::move(notification_batch))); } } FilePathWatcherImpl::~FilePathWatcherImpl() { DCHECK(!task_runner() || task_runner()->RunsTasksInCurrentSequence()); } bool FilePathWatcherImpl::Watch(const FilePath& path, Type type, const FilePathWatcher::Callback& callback) { return WatchWithChangeInfo( path, WatchOptions{.type = type}, base::IgnoreArgs( base::BindRepeating(std::move(callback)))); } bool FilePathWatcherImpl::WatchWithOptions( const FilePath& path, const WatchOptions& options, const FilePathWatcher::Callback& callback) { return WatchWithChangeInfo( path, options, base::IgnoreArgs( base::BindRepeating(std::move(callback)))); } bool FilePathWatcherImpl::WatchWithChangeInfo( const FilePath& path, const WatchOptions& options, const FilePathWatcher::CallbackWithChangeInfo& callback) { DCHECK(target_.empty()); // Can only watch one path. set_task_runner(SequencedTaskRunner::GetCurrentDefault()); callback_ = callback; target_ = path; type_ = options.type; File::Info file_info; target_exists_ = GetFileInfo(target_, &file_info); return SetupWatchHandleForTarget(); } void FilePathWatcherImpl::Cancel() { set_cancelled(); if (callback_.is_null()) { // Watch was never called, or the `task_runner_` has already quit. return; } DCHECK(task_runner()->RunsTasksInCurrentSequence()); CloseWatchHandle(); callback_.Reset(); } Lock& FilePathWatcherImpl::GetWatchThreadLockForTest() { return CompletionIOPortThread::Get()->GetLockForTest(); // IN-TEST } void FilePathWatcherImpl::BufferOverflowed() { // `this` may be deleted after `callback_` is run. callback_.Run(FilePathWatcher::ChangeInfo(), target_, /*error=*/false); } void FilePathWatcherImpl::WatchedDirectoryDeleted( base::FilePath watched_path, base::HeapArray notification_batch) { if (!SetupWatchHandleForTarget()) { // `this` may be deleted after `callback_` is run. callback_.Run(FilePathWatcher::ChangeInfo(), target_, /*error=*/true); return; } if (!notification_batch.empty()) { auto self = weak_factory_.GetWeakPtr(); // `ProcessNotificationBatch` may delete `this`. ProcessNotificationBatch(std::move(watched_path), std::move(notification_batch)); if (!self) { return; } } bool target_was_deleted = target_exists_ || watched_path == target_; if (target_was_deleted) { // `this` may be deleted after `callback_` is run. callback_.Run(FilePathWatcher::ChangeInfo(), target_, /*error=*/false); } } void FilePathWatcherImpl::ProcessNotificationBatch( base::FilePath watched_path, base::HeapArray notification_batch) { DCHECK(task_runner()->RunsTasksInCurrentSequence()); CHECK(!notification_batch.empty()); auto self = weak_factory_.GetWeakPtr(); // Check whether the event applies to `target_` and notify the callback. File::Info target_info; bool target_exists_after_batch = GetFileInfo(target_, &target_info); bool target_created_or_deleted = target_exists_after_batch != target_exists_; target_exists_ = target_exists_after_batch; // This keeps track of whether we just notified for a // `FILE_ACTION_RENAMED_OLD_NAME`. bool last_event_notified_for_old_name = false; auto sub_span = notification_batch.as_span(); bool has_next_entry = true; while (has_next_entry) { const auto& file_notify_info = *reinterpret_cast(sub_span.data()); has_next_entry = file_notify_info.NextEntryOffset != 0; if (has_next_entry) { sub_span = sub_span.subspan(file_notify_info.NextEntryOffset); } DWORD change_type = file_notify_info.Action; // A rename will generate two move events, but we only report it as one move // event. So continue if we just reported a `FILE_ACTION_RENAMED_OLD_NAME`. if (last_event_notified_for_old_name && change_type == FILE_ACTION_RENAMED_NEW_NAME) { last_event_notified_for_old_name = false; continue; } last_event_notified_for_old_name = false; FilePath change_path = watched_path.Append(std::basic_string_view( file_notify_info.FileName, file_notify_info.FileNameLength / sizeof(wchar_t))); // Ancestors of the `target_` are outside the watch scope. if (change_path.IsParent(target_)) { // Only report move events where the target was created or deleted. if ((change_type != FILE_ACTION_RENAMED_NEW_NAME && change_type != FILE_ACTION_RENAMED_OLD_NAME) || !target_created_or_deleted) { continue; } } else if (type_ == FilePathWatcher::Type::kNonRecursive && change_path != target_ && change_path.DirName() != target_) { // For non recursive watches, only report events for the target or its // direct children. continue; } if (change_type == FILE_ACTION_MODIFIED) { // Don't report modified events for directories. File::Info file_info; if (GetFileInfo(change_path, &file_info) && file_info.is_directory) { continue; } } last_event_notified_for_old_name = change_type == FILE_ACTION_RENAMED_OLD_NAME; // `this` may be deleted after `callback_` is run. callback_.Run(FilePathWatcher::ChangeInfo(), target_, /*error=*/false); if (!self) { return; } } } bool FilePathWatcherImpl::SetupWatchHandleForTarget() { CloseWatchHandle(); ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK); // Start at the target and walk up the directory chain until we successfully // create a file handle in `watched_handle_`. `child_dirs` keeps a stack of // child directories stripped from target, in reverse order. std::vector child_dirs; FilePath path_to_watch(target_); base::win::ScopedHandle watched_handle; FilePath watched_path; while (true) { auto result = CreateDirectoryHandle(path_to_watch); // Break if a valid handle is returned. if (result.has_value()) { watched_handle = std::move(result.value()); watched_path = path_to_watch; break; } // We're in an unknown state if `CreateDirectoryHandle` returns an `kFatal` // error, so return failure. if (result.error() == CreateFileHandleError::kFatal) { return false; } // Abort if we hit the root directory. child_dirs.push_back(path_to_watch.BaseName()); FilePath parent(path_to_watch.DirName()); if (parent == path_to_watch) { DLOG(ERROR) << "Reached the root directory"; return false; } path_to_watch = std::move(parent); } // At this point, `watched_handle` is valid. However, the bottom-up search // that the above code performs races against directory creation. So try to // walk back down and see whether any children appeared in the mean time. while (!child_dirs.empty()) { path_to_watch = path_to_watch.Append(child_dirs.back()); child_dirs.pop_back(); auto result = CreateDirectoryHandle(path_to_watch); if (!result.has_value()) { // We're in an unknown state if `CreateDirectoryHandle` returns an // `kFatal` error, so return failure. if (result.error() == CreateFileHandleError::kFatal) { return false; } // Otherwise go with the current `watched_handle`. break; } watched_handle = std::move(result.value()); watched_path = path_to_watch; } watcher_id_ = CompletionIOPortThread::Get()->AddWatcher( *this, std::move(watched_handle), std::move(watched_path)); return watcher_id_.has_value(); } void FilePathWatcherImpl::CloseWatchHandle() { if (watcher_id_.has_value()) { CompletionIOPortThread::Get()->RemoveWatcher(watcher_id_.value()); watcher_id_.reset(); } } } // namespace FilePathWatcher::FilePathWatcher() : FilePathWatcher(std::make_unique()) {} } // namespace base