blob: 7c774877987a3495bff3d24487f9f8ff6c659d1c [file] [log] [blame]
// 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/scoped_blocking_call.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/time/time.h"
#include "base/win/object_watcher.h"
#include <windows.h>
#include "starboard/types.h"
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;
ScopedBlockingCall scoped_blocking_call(BlockingType::MAY_BLOCK);
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 = false;
{
ScopedBlockingCall scoped_blocking_call(BlockingType::MAY_BLOCK);
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) {
ScopedBlockingCall scoped_blocking_call(BlockingType::MAY_BLOCK);
*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();
ScopedBlockingCall scoped_blocking_call(BlockingType::MAY_BLOCK);
// 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<FilePath> 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();
ScopedBlockingCall scoped_blocking_call(BlockingType::MAY_BLOCK);
FindCloseChangeNotification(handle_);
handle_ = INVALID_HANDLE_VALUE;
}
} // namespace
FilePathWatcher::FilePathWatcher() {
sequence_checker_.DetachFromSequence();
impl_ = std::make_unique<FilePathWatcherImpl>();
}
} // namespace base