blob: 9d2a946b65eaaf2c896c6a5dedfca30cb6571dd3 [file] [log] [blame]
// 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 <windows.h>
#include <winnt.h>
#include <cstdint>
#include <map>
#include <memory>
#include <tuple>
#include <utility>
#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<base::win::ScopedHandle, CreateFileHandleError>
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<class WatcherEntryIdTag>;
CompletionIOPortThread(const CompletionIOPortThread&) = delete;
CompletionIOPortThread& operator=(const CompletionIOPortThread&) = delete;
static CompletionIOPortThread* Get() {
static NoDestructor<CompletionIOPortThread> io_thread;
return io_thread.get();
}
// Thread safe.
std::optional<WatcherEntryId> 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<CompletionIOPortThread>;
// 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<FilePathWatcherImpl> watcher_weak_ptr,
scoped_refptr<SequencedTaskRunner> 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<FilePathWatcherImpl> watcher_weak_ptr;
scoped_refptr<SequencedTaskRunner> 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<WatcherEntryId, WatcherEntry> 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<ULONG_PTR>(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<uint8_t> notification_batch);
void ProcessNotificationBatch(base::FilePath watched_path,
base::HeapArray<uint8_t> notification_batch);
// Callback to notify upon changes.
FilePathWatcher::CallbackWithChangeInfo callback_;
// Path we're supposed to watch (passed to callback).
FilePath target_;
std::optional<CompletionIOPortThread::WatcherEntryId> watcher_id_;
// The type of watch requested.
Type type_ = Type::kNonRecursive;
bool target_exists_ = false;
WeakPtrFactory<FilePathWatcherImpl> 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::WatcherEntryId>
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<ULONG_PTR>(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<ULONG_PTR>(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<DWORD>(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<uint8_t>()));
continue;
}
base::HeapArray<uint8_t> notification_batch;
if (bytes_transferred > 0) {
notification_batch = base::HeapArray<uint8_t>::CopiedFrom(
base::span<uint8_t>(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<DWORD>(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<const FilePathWatcher::ChangeInfo&>(
base::BindRepeating(std::move(callback))));
}
bool FilePathWatcherImpl::WatchWithOptions(
const FilePath& path,
const WatchOptions& options,
const FilePathWatcher::Callback& callback) {
return WatchWithChangeInfo(
path, options,
base::IgnoreArgs<const FilePathWatcher::ChangeInfo&>(
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<uint8_t> 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<uint8_t> 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<FILE_NOTIFY_INFORMATION*>(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<wchar_t>(
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<FilePath> 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<FilePathWatcherImpl>()) {}
} // namespace base