// Copyright (c) 2011 The Chromium Authors. All rights reserved. // 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 "base/bind.h" #include "base/files/file.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/logging.h" #include "base/macros.h" #include "base/memory/ptr_util.h" #include "base/threading/sequenced_task_runner_handle.h" #include "base/time/time.h" #include "base/win/object_watcher.h" #include namespace base { namespace { class FilePathWatcherImpl : public FilePathWatcher::PlatformDelegate, public base::win::ObjectWatcher::Delegate { public: FilePathWatcherImpl() : handle_(INVALID_HANDLE_VALUE), recursive_watch_(false) {} ~FilePathWatcherImpl() override; // FilePathWatcher::PlatformDelegate: bool Watch(const FilePath& path, bool recursive, const FilePathWatcher::Callback& callback) override; void Cancel() override; // base::win::ObjectWatcher::Delegate: void OnObjectSignaled(HANDLE object) override; private: // Setup a watch handle for directory |dir|. Set |recursive| to true to watch // the directory sub trees. Returns true if no fatal error occurs. |handle| // will receive the handle value if |dir| is watchable, otherwise // INVALID_HANDLE_VALUE. static bool SetupWatchHandle(const FilePath& dir, bool recursive, HANDLE* handle) WARN_UNUSED_RESULT; // (Re-)Initialize the watch handle. bool UpdateWatch() WARN_UNUSED_RESULT; // Destroy the watch handle. void DestroyWatch(); // Callback to notify upon changes. FilePathWatcher::Callback callback_; // Path we're supposed to watch (passed to callback). FilePath target_; // Set to true in the destructor. bool* was_deleted_ptr_ = nullptr; // Handle for FindFirstChangeNotification. HANDLE handle_; // ObjectWatcher to watch handle_ for events. base::win::ObjectWatcher watcher_; // Set to true to watch the sub trees of the specified directory file path. bool recursive_watch_; // Keep track of the last modified time of the file. We use nulltime // to represent the file not existing. Time last_modified_; // The time at which we processed the first notification with the // |last_modified_| time stamp. Time first_notification_; DISALLOW_COPY_AND_ASSIGN(FilePathWatcherImpl); }; FilePathWatcherImpl::~FilePathWatcherImpl() { DCHECK(!task_runner() || task_runner()->RunsTasksInCurrentSequence()); if (was_deleted_ptr_) *was_deleted_ptr_ = true; } bool FilePathWatcherImpl::Watch(const FilePath& path, bool recursive, const FilePathWatcher::Callback& callback) { DCHECK(target_.value().empty()); // Can only watch one path. set_task_runner(SequencedTaskRunnerHandle::Get()); callback_ = callback; target_ = path; recursive_watch_ = recursive; File::Info file_info; if (GetFileInfo(target_, &file_info)) { last_modified_ = file_info.last_modified; first_notification_ = Time::Now(); } if (!UpdateWatch()) return false; watcher_.StartWatchingOnce(handle_, this); return true; } void FilePathWatcherImpl::Cancel() { if (callback_.is_null()) { // Watch was never called, or the |task_runner_| has already quit. set_cancelled(); return; } DCHECK(task_runner()->RunsTasksInCurrentSequence()); set_cancelled(); if (handle_ != INVALID_HANDLE_VALUE) DestroyWatch(); callback_.Reset(); } void FilePathWatcherImpl::OnObjectSignaled(HANDLE object) { DCHECK(task_runner()->RunsTasksInCurrentSequence()); DCHECK_EQ(object, handle_); DCHECK(!was_deleted_ptr_); bool was_deleted = false; was_deleted_ptr_ = &was_deleted; if (!UpdateWatch()) { callback_.Run(target_, true /* error */); return; } // Check whether the event applies to |target_| and notify the callback. File::Info file_info; bool file_exists = GetFileInfo(target_, &file_info); if (recursive_watch_) { // Only the mtime of |target_| is tracked but in a recursive watch, // some other file or directory may have changed so all notifications // are passed through. It is possible to figure out which file changed // using ReadDirectoryChangesW() instead of FindFirstChangeNotification(), // but that function is quite complicated: // http://qualapps.blogspot.com/2010/05/understanding-readdirectorychangesw.html callback_.Run(target_, false); } else if (file_exists && (last_modified_.is_null() || last_modified_ != file_info.last_modified)) { last_modified_ = file_info.last_modified; first_notification_ = Time::Now(); callback_.Run(target_, false); } else if (file_exists && last_modified_ == file_info.last_modified && !first_notification_.is_null()) { // The target's last modification time is equal to what's on record. This // means that either an unrelated event occurred, or the target changed // again (file modification times only have a resolution of 1s). Comparing // file modification times against the wall clock is not reliable to find // out whether the change is recent, since this code might just run too // late. Moreover, there's no guarantee that file modification time and wall // clock times come from the same source. // // Instead, the time at which the first notification carrying the current // |last_notified_| time stamp is recorded. Later notifications that find // the same file modification time only need to be forwarded until wall // clock has advanced one second from the initial notification. After that // interval, client code is guaranteed to having seen the current revision // of the file. if (Time::Now() - first_notification_ > TimeDelta::FromSeconds(1)) { // Stop further notifications for this |last_modification_| time stamp. first_notification_ = Time(); } callback_.Run(target_, false); } else if (!file_exists && !last_modified_.is_null()) { last_modified_ = Time(); callback_.Run(target_, false); } // The watch may have been cancelled by the callback. if (!was_deleted) { watcher_.StartWatchingOnce(handle_, this); was_deleted_ptr_ = nullptr; } } // static bool FilePathWatcherImpl::SetupWatchHandle(const FilePath& dir, bool recursive, HANDLE* handle) { *handle = FindFirstChangeNotification( dir.value().c_str(), recursive, 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); if (*handle != INVALID_HANDLE_VALUE) { // Make sure the handle we got points to an existing directory. It seems // that windows sometimes hands out watches to directories that are // about to go away, but doesn't sent notifications if that happens. if (!DirectoryExists(dir)) { FindCloseChangeNotification(*handle); *handle = INVALID_HANDLE_VALUE; } return true; } // If FindFirstChangeNotification failed because 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, try the // immediate parent directory instead. DWORD error_code = GetLastError(); if (error_code != ERROR_FILE_NOT_FOUND && error_code != ERROR_PATH_NOT_FOUND && error_code != ERROR_ACCESS_DENIED && error_code != ERROR_SHARING_VIOLATION && error_code != ERROR_DIRECTORY) { DPLOG(ERROR) << "FindFirstChangeNotification failed for " << dir.value(); return false; } return true; } bool FilePathWatcherImpl::UpdateWatch() { if (handle_ != INVALID_HANDLE_VALUE) DestroyWatch(); // Start at the target and walk up the directory chain until we succesfully // create a watch handle in |handle_|. |child_dirs| keeps a stack of child // directories stripped from target, in reverse order. std::vector child_dirs; FilePath watched_path(target_); while (true) { if (!SetupWatchHandle(watched_path, recursive_watch_, &handle_)) return false; // Break if a valid handle is returned. Try the parent directory otherwise. if (handle_ != INVALID_HANDLE_VALUE) break; // Abort if we hit the root directory. child_dirs.push_back(watched_path.BaseName()); FilePath parent(watched_path.DirName()); if (parent == watched_path) { DLOG(ERROR) << "Reached the root directory"; return false; } watched_path = parent; } // At this point, 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()) { watched_path = watched_path.Append(child_dirs.back()); child_dirs.pop_back(); HANDLE temp_handle = INVALID_HANDLE_VALUE; if (!SetupWatchHandle(watched_path, recursive_watch_, &temp_handle)) return false; if (temp_handle == INVALID_HANDLE_VALUE) break; FindCloseChangeNotification(handle_); handle_ = temp_handle; } return true; } void FilePathWatcherImpl::DestroyWatch() { watcher_.StopWatching(); FindCloseChangeNotification(handle_); handle_ = INVALID_HANDLE_VALUE; } } // namespace FilePathWatcher::FilePathWatcher() { sequence_checker_.DetachFromSequence(); impl_ = std::make_unique(); } } // namespace base