blob: 559af7ce884000a9e436bf1148ba40e225df7396 [file] [log] [blame]
// Copyright (c) 2015 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 "chrome/browser/devtools/devtools_file_watcher.h"
#include <algorithm>
#include <map>
#include <memory>
#include <set>
#include <unordered_map>
#include "base/bind.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/memory/ref_counted.h"
#include "base/sequenced_task_runner.h"
#include "base/task/lazy_task_runner.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "content/public/browser/browser_thread.h"
using content::BrowserThread;
static constexpr int kFirstThrottleTimeout = 10;
static constexpr int kDefaultThrottleTimeout = 200;
// DevToolsFileWatcher::SharedFileWatcher --------------------------------------
class DevToolsFileWatcher::SharedFileWatcher :
public base::RefCounted<SharedFileWatcher> {
public:
SharedFileWatcher();
void AddListener(DevToolsFileWatcher* watcher);
void RemoveListener(DevToolsFileWatcher* watcher);
void AddWatch(const base::FilePath& path);
void RemoveWatch(const base::FilePath& path);
private:
friend class base::RefCounted<
DevToolsFileWatcher::SharedFileWatcher>;
~SharedFileWatcher();
using FilePathTimesMap = std::unordered_map<base::FilePath, base::Time>;
FilePathTimesMap GetModificationTimes(const base::FilePath& path);
void DirectoryChanged(const base::FilePath& path, bool error);
void DispatchNotifications();
std::vector<DevToolsFileWatcher*> listeners_;
std::map<base::FilePath, std::unique_ptr<base::FilePathWatcher>> watchers_;
std::map<base::FilePath, FilePathTimesMap> file_path_times_;
std::set<base::FilePath> pending_paths_;
base::Time last_event_time_;
base::TimeDelta last_dispatch_cost_;
SEQUENCE_CHECKER(sequence_checker_);
};
DevToolsFileWatcher::SharedFileWatcher::SharedFileWatcher()
: last_dispatch_cost_(
base::TimeDelta::FromMilliseconds(kDefaultThrottleTimeout)) {
DevToolsFileWatcher::s_shared_watcher_ = this;
}
DevToolsFileWatcher::SharedFileWatcher::~SharedFileWatcher() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DevToolsFileWatcher::s_shared_watcher_ = nullptr;
}
void DevToolsFileWatcher::SharedFileWatcher::AddListener(
DevToolsFileWatcher* watcher) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
listeners_.push_back(watcher);
}
void DevToolsFileWatcher::SharedFileWatcher::RemoveListener(
DevToolsFileWatcher* watcher) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = std::find(listeners_.begin(), listeners_.end(), watcher);
listeners_.erase(it);
if (listeners_.empty()) {
file_path_times_.clear();
pending_paths_.clear();
}
}
void DevToolsFileWatcher::SharedFileWatcher::AddWatch(
const base::FilePath& path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (watchers_.find(path) != watchers_.end())
return;
if (!base::FilePathWatcher::RecursiveWatchAvailable())
return;
watchers_[path].reset(new base::FilePathWatcher());
bool success = watchers_[path]->Watch(
path, true,
base::Bind(&SharedFileWatcher::DirectoryChanged, base::Unretained(this)));
if (!success)
return;
file_path_times_[path] = GetModificationTimes(path);
}
DevToolsFileWatcher::SharedFileWatcher::FilePathTimesMap
DevToolsFileWatcher::SharedFileWatcher::GetModificationTimes(
const base::FilePath& path) {
FilePathTimesMap times_map;
base::FileEnumerator enumerator(path, true, base::FileEnumerator::FILES);
base::FilePath file_path = enumerator.Next();
while (!file_path.empty()) {
base::FileEnumerator::FileInfo file_info = enumerator.GetInfo();
times_map[std::move(file_path)] = file_info.GetLastModifiedTime();
file_path = enumerator.Next();
}
return times_map;
}
void DevToolsFileWatcher::SharedFileWatcher::RemoveWatch(
const base::FilePath& path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
watchers_.erase(path);
file_path_times_.erase(path);
}
void DevToolsFileWatcher::SharedFileWatcher::DirectoryChanged(
const base::FilePath& path,
bool error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
pending_paths_.insert(path);
if (pending_paths_.size() > 1)
return; // PostDelayedTask is already pending.
base::Time now = base::Time::Now();
// Quickly dispatch first chunk.
base::TimeDelta shedule_for =
now - last_event_time_ > last_dispatch_cost_ ?
base::TimeDelta::FromMilliseconds(kFirstThrottleTimeout) :
last_dispatch_cost_ * 2;
base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&DevToolsFileWatcher::SharedFileWatcher::DispatchNotifications, this),
shedule_for);
last_event_time_ = now;
}
void DevToolsFileWatcher::SharedFileWatcher::DispatchNotifications() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!pending_paths_.size())
return;
base::Time start = base::Time::Now();
std::vector<std::string> added_paths;
std::vector<std::string> removed_paths;
std::vector<std::string> changed_paths;
for (const auto& path : pending_paths_) {
FilePathTimesMap& old_times = file_path_times_[path];
FilePathTimesMap current_times = GetModificationTimes(path);
for (const auto& path_time : current_times) {
const base::FilePath& path = path_time.first;
auto old_timestamp = old_times.find(path);
if (old_timestamp == old_times.end())
added_paths.push_back(path.AsUTF8Unsafe());
else if (old_timestamp->second != path_time.second)
changed_paths.push_back(path.AsUTF8Unsafe());
}
for (const auto& path_time : old_times) {
const base::FilePath& path = path_time.first;
if (current_times.find(path) == current_times.end())
removed_paths.push_back(path.AsUTF8Unsafe());
}
old_times.swap(current_times);
}
pending_paths_.clear();
for (auto* watcher : listeners_) {
watcher->client_task_runner_->PostTask(
FROM_HERE, base::BindOnce(watcher->callback_, changed_paths,
added_paths, removed_paths));
}
last_dispatch_cost_ = base::Time::Now() - start;
}
// DevToolsFileWatcher ---------------------------------------------------------
namespace {
base::SequencedTaskRunner* impl_task_runner() {
constexpr base::TaskTraits kImplTaskTraits = {
base::MayBlock(), base::TaskPriority::BEST_EFFORT};
static base::LazySequencedTaskRunner s_file_task_runner =
LAZY_SEQUENCED_TASK_RUNNER_INITIALIZER(kImplTaskTraits);
return s_file_task_runner.Get().get();
}
} // namespace
// static
DevToolsFileWatcher::SharedFileWatcher*
DevToolsFileWatcher::s_shared_watcher_ = nullptr;
// static
void DevToolsFileWatcher::Deleter::operator()(const DevToolsFileWatcher* ptr) {
impl_task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&DevToolsFileWatcher::Destroy, base::Unretained(ptr)));
}
DevToolsFileWatcher::DevToolsFileWatcher(
WatchCallback callback,
scoped_refptr<base::SequencedTaskRunner> callback_task_runner)
: callback_(std::move(callback)),
client_task_runner_(std::move(callback_task_runner)) {
impl_task_runner()->PostTask(
FROM_HERE, base::BindOnce(&DevToolsFileWatcher::InitSharedWatcher,
base::Unretained(this)));
}
DevToolsFileWatcher::~DevToolsFileWatcher() {
DCHECK(impl_task_runner()->RunsTasksInCurrentSequence());
shared_watcher_->RemoveListener(this);
}
void DevToolsFileWatcher::InitSharedWatcher() {
if (!DevToolsFileWatcher::s_shared_watcher_)
new SharedFileWatcher();
shared_watcher_ = DevToolsFileWatcher::s_shared_watcher_;
shared_watcher_->AddListener(this);
}
void DevToolsFileWatcher::AddWatch(base::FilePath path) {
impl_task_runner()->PostTask(
FROM_HERE, base::BindOnce(&DevToolsFileWatcher::AddWatchOnImpl,
base::Unretained(this), std::move(path)));
}
void DevToolsFileWatcher::RemoveWatch(base::FilePath path) {
impl_task_runner()->PostTask(
FROM_HERE, base::BindOnce(&DevToolsFileWatcher::RemoveWatchOnImpl,
base::Unretained(this), std::move(path)));
}
void DevToolsFileWatcher::AddWatchOnImpl(base::FilePath path) {
shared_watcher_->AddWatch(std::move(path));
}
void DevToolsFileWatcher::RemoveWatchOnImpl(base::FilePath path) {
shared_watcher_->RemoveWatch(std::move(path));
}