| // 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)); |
| } |