| // Copyright 2017 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 "content/browser/devtools/protocol/target_auto_attacher.h" |
| |
| #include "base/containers/queue.h" |
| #include "content/browser/devtools/devtools_renderer_channel.h" |
| #include "content/browser/devtools/render_frame_devtools_agent_host.h" |
| #include "content/browser/devtools/service_worker_devtools_agent_host.h" |
| #include "content/browser/frame_host/frame_tree.h" |
| #include "content/browser/frame_host/frame_tree_node.h" |
| #include "content/browser/frame_host/navigation_handle_impl.h" |
| #include "content/browser/frame_host/navigation_request.h" |
| #include "content/browser/frame_host/render_frame_host_impl.h" |
| |
| namespace content { |
| namespace protocol { |
| |
| namespace { |
| |
| using ScopeAgentsMap = |
| std::map<GURL, std::unique_ptr<ServiceWorkerDevToolsAgentHost::List>>; |
| |
| void GetMatchingHostsByScopeMap( |
| const ServiceWorkerDevToolsAgentHost::List& agent_hosts, |
| const base::flat_set<GURL>& urls, |
| ScopeAgentsMap* scope_agents_map) { |
| base::flat_set<GURL> host_name_set; |
| for (const GURL& url : urls) |
| host_name_set.insert(url.GetOrigin()); |
| for (const auto& host : agent_hosts) { |
| if (host_name_set.find(host->scope().GetOrigin()) == host_name_set.end()) |
| continue; |
| const auto& it = scope_agents_map->find(host->scope()); |
| if (it == scope_agents_map->end()) { |
| std::unique_ptr<ServiceWorkerDevToolsAgentHost::List> new_list( |
| new ServiceWorkerDevToolsAgentHost::List()); |
| new_list->push_back(host); |
| (*scope_agents_map)[host->scope()] = std::move(new_list); |
| } else { |
| it->second->push_back(host); |
| } |
| } |
| } |
| |
| void AddEligibleHosts(const ServiceWorkerDevToolsAgentHost::List& list, |
| ServiceWorkerDevToolsAgentHost::Map* result) { |
| base::Time last_installed_time; |
| base::Time last_doomed_time; |
| for (const auto& host : list) { |
| if (host->version_installed_time() > last_installed_time) |
| last_installed_time = host->version_installed_time(); |
| if (host->version_doomed_time() > last_doomed_time) |
| last_doomed_time = host->version_doomed_time(); |
| } |
| for (const auto& host : list) { |
| // We don't attech old redundant Service Workers when there is newer |
| // installed Service Worker. |
| if (host->version_doomed_time().is_null() || |
| (last_installed_time < last_doomed_time && |
| last_doomed_time == host->version_doomed_time())) { |
| (*result)[host->GetId()] = host; |
| } |
| } |
| } |
| |
| ServiceWorkerDevToolsAgentHost::Map GetMatchingServiceWorkers( |
| BrowserContext* browser_context, |
| const base::flat_set<GURL>& urls) { |
| ServiceWorkerDevToolsAgentHost::Map result; |
| if (!browser_context) |
| return result; |
| |
| ServiceWorkerDevToolsAgentHost::List agent_hosts; |
| ServiceWorkerDevToolsManager::GetInstance() |
| ->AddAllAgentHostsForBrowserContext(browser_context, &agent_hosts); |
| |
| ScopeAgentsMap scope_agents_map; |
| GetMatchingHostsByScopeMap(agent_hosts, urls, &scope_agents_map); |
| |
| for (const auto& it : scope_agents_map) |
| AddEligibleHosts(*it.second.get(), &result); |
| |
| return result; |
| } |
| |
| base::flat_set<GURL> GetFrameUrls(RenderFrameHostImpl* render_frame_host) { |
| // We try to attach to a service worker in the following cases: |
| // 1. SW is created while user is inspecting frame (from WorkerCreated). |
| // 2. Frame has navigated and we are picking up new SW corresponding to new |
| // url (from DidFinishNavigation). |
| // 3. Frame is trying to navigate and it spawns a new SW which we pick up |
| // (from WorkerCreated). See also https://crbug.com/907072 |
| // |
| // We are not attaching in the following case: |
| // 4. Frame is trying to navigate and we _should_ pick up an existing SW but we don't. |
| // We _could_ do this, but since we are not pausing the navigation, there |
| // is no principal difference between picking up SW earlier or later. |
| // |
| // We also try to detach from SW picked up for [3] if navigation has failed |
| // (from DidFinishNavigation). |
| |
| base::flat_set<GURL> frame_urls; |
| if (render_frame_host) { |
| for (FrameTreeNode* node : |
| render_frame_host->frame_tree_node()->frame_tree()->Nodes()) { |
| frame_urls.insert(node->current_url()); |
| // We use both old and new frame urls to support [3], where we attach while |
| // navigation is still ongoing. |
| if (node->navigation_request()) { |
| frame_urls.insert(node->navigation_request()->common_params().url); |
| } |
| } |
| } |
| return frame_urls; |
| } |
| |
| } // namespace |
| |
| TargetAutoAttacher::TargetAutoAttacher( |
| AttachCallback attach_callback, |
| DetachCallback detach_callback, |
| DevToolsRendererChannel* renderer_channel) |
| : attach_callback_(attach_callback), |
| detach_callback_(detach_callback), |
| renderer_channel_(renderer_channel), |
| render_frame_host_(nullptr), |
| auto_attach_(false), |
| wait_for_debugger_on_start_(false) {} |
| |
| TargetAutoAttacher::~TargetAutoAttacher() {} |
| |
| void TargetAutoAttacher::SetRenderFrameHost( |
| RenderFrameHostImpl* render_frame_host) { |
| render_frame_host_ = render_frame_host; |
| UpdateFrames(); |
| ReattachServiceWorkers(false); |
| } |
| |
| void TargetAutoAttacher::UpdateServiceWorkers() { |
| ReattachServiceWorkers(false); |
| } |
| |
| void TargetAutoAttacher::UpdateFrames() { |
| if (!auto_attach_) |
| return; |
| |
| Hosts new_hosts; |
| if (render_frame_host_) { |
| FrameTreeNode* root = render_frame_host_->frame_tree_node(); |
| base::queue<FrameTreeNode*> queue; |
| queue.push(root); |
| while (!queue.empty()) { |
| FrameTreeNode* node = queue.front(); |
| queue.pop(); |
| bool cross_process = node->current_frame_host()->IsCrossProcessSubframe(); |
| if (node != root && cross_process) { |
| scoped_refptr<DevToolsAgentHost> new_host = |
| RenderFrameDevToolsAgentHost::GetOrCreateFor(node); |
| new_hosts.insert(new_host); |
| } else { |
| for (size_t i = 0; i < node->child_count(); ++i) |
| queue.push(node->child_at(i)); |
| } |
| } |
| } |
| |
| // TODO(dgozman): support wait_for_debugger_on_start_. |
| ReattachTargetsOfType(new_hosts, DevToolsAgentHost::kTypeFrame, false); |
| } |
| |
| void TargetAutoAttacher::AgentHostClosed(DevToolsAgentHost* host) { |
| auto_attached_hosts_.erase(base::WrapRefCounted(host)); |
| } |
| |
| bool TargetAutoAttacher::ShouldThrottleFramesNavigation() { |
| return auto_attach_; |
| } |
| |
| DevToolsAgentHost* TargetAutoAttacher::AutoAttachToFrame( |
| NavigationHandleImpl* navigation_handle) { |
| if (!ShouldThrottleFramesNavigation()) |
| return nullptr; |
| |
| FrameTreeNode* frame_tree_node = navigation_handle->frame_tree_node(); |
| RenderFrameHostImpl* new_host = navigation_handle->GetRenderFrameHost(); |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| RenderFrameDevToolsAgentHost::FindForDangling(frame_tree_node); |
| |
| bool old_cross_process = !!agent_host; |
| bool new_cross_process = new_host && new_host->IsCrossProcessSubframe(); |
| |
| if (old_cross_process == new_cross_process) |
| return nullptr; |
| |
| if (new_cross_process) { |
| agent_host = |
| RenderFrameDevToolsAgentHost::GetOrCreateForDangling(frame_tree_node); |
| DCHECK(auto_attached_hosts_.find(agent_host) == auto_attached_hosts_.end()); |
| attach_callback_.Run(agent_host.get(), wait_for_debugger_on_start_); |
| auto_attached_hosts_.insert(agent_host); |
| return wait_for_debugger_on_start_ ? agent_host.get() : nullptr; |
| } |
| |
| DCHECK(old_cross_process); |
| auto it = auto_attached_hosts_.find(agent_host); |
| // This should not happen in theory, but error pages are sometimes not |
| // picked up. See https://crbug.com/836511 and https://crbug.com/817881. |
| if (it == auto_attached_hosts_.end()) |
| return nullptr; |
| auto_attached_hosts_.erase(it); |
| detach_callback_.Run(agent_host.get()); |
| return nullptr; |
| } |
| |
| void TargetAutoAttacher::ReattachServiceWorkers(bool waiting_for_debugger) { |
| if (!auto_attaching_service_workers_) |
| return; |
| |
| BrowserContext* browser_context = nullptr; |
| if (render_frame_host_) |
| browser_context = render_frame_host_->GetProcess()->GetBrowserContext(); |
| |
| auto matching = GetMatchingServiceWorkers(browser_context, |
| GetFrameUrls(render_frame_host_)); |
| Hosts new_hosts; |
| for (const auto& pair : matching) |
| new_hosts.insert(pair.second); |
| ReattachTargetsOfType(new_hosts, DevToolsAgentHost::kTypeServiceWorker, |
| waiting_for_debugger); |
| } |
| |
| void TargetAutoAttacher::ReattachTargetsOfType(const Hosts& new_hosts, |
| const std::string& type, |
| bool waiting_for_debugger) { |
| Hosts old_hosts = auto_attached_hosts_; |
| for (auto& host : old_hosts) { |
| if (host->GetType() == type && new_hosts.find(host) == new_hosts.end()) { |
| auto_attached_hosts_.erase(host); |
| detach_callback_.Run(host.get()); |
| } |
| } |
| for (auto& host : new_hosts) { |
| if (old_hosts.find(host) == old_hosts.end()) { |
| attach_callback_.Run(host.get(), waiting_for_debugger); |
| auto_attached_hosts_.insert(host); |
| } |
| } |
| } |
| |
| void TargetAutoAttacher::SetAutoAttach(bool auto_attach, |
| bool wait_for_debugger_on_start, |
| base::OnceClosure callback) { |
| wait_for_debugger_on_start_ = wait_for_debugger_on_start; |
| if (auto_attach && !auto_attach_) { |
| auto_attach_ = true; |
| auto_attaching_service_workers_ = |
| render_frame_host_ && !render_frame_host_->GetParent(); |
| if (auto_attaching_service_workers_) { |
| ServiceWorkerDevToolsManager::GetInstance()->AddObserver(this); |
| ReattachServiceWorkers(false); |
| } |
| UpdateFrames(); |
| } else if (!auto_attach && auto_attach_) { |
| auto_attach_ = false; |
| Hosts empty; |
| ReattachTargetsOfType(empty, DevToolsAgentHost::kTypeFrame, false); |
| if (auto_attaching_service_workers_) { |
| ServiceWorkerDevToolsManager::GetInstance()->RemoveObserver(this); |
| ReattachTargetsOfType(empty, DevToolsAgentHost::kTypeServiceWorker, |
| false); |
| auto_attaching_service_workers_ = false; |
| } |
| ReattachTargetsOfType(empty, DevToolsAgentHost::kTypeDedicatedWorker, |
| false); |
| DCHECK(auto_attached_hosts_.empty()); |
| } |
| renderer_channel_->SetReportChildWorkers( |
| this, auto_attach, wait_for_debugger_on_start, std::move(callback)); |
| } |
| |
| // -------- ServiceWorkerDevToolsManager::Observer ---------- |
| |
| void TargetAutoAttacher::WorkerCreated(ServiceWorkerDevToolsAgentHost* host, |
| bool* should_pause_on_start) { |
| BrowserContext* browser_context = nullptr; |
| if (render_frame_host_) |
| browser_context = render_frame_host_->GetProcess()->GetBrowserContext(); |
| |
| auto hosts = GetMatchingServiceWorkers(browser_context, |
| GetFrameUrls(render_frame_host_)); |
| if (hosts.find(host->GetId()) != hosts.end()) { |
| *should_pause_on_start = wait_for_debugger_on_start_; |
| Hosts new_hosts; |
| for (const auto& pair : hosts) |
| new_hosts.insert(pair.second); |
| ReattachTargetsOfType(new_hosts, DevToolsAgentHost::kTypeServiceWorker, |
| wait_for_debugger_on_start_); |
| } |
| } |
| |
| void TargetAutoAttacher::WorkerVersionInstalled( |
| ServiceWorkerDevToolsAgentHost* host) { |
| ReattachServiceWorkers(false); |
| } |
| |
| void TargetAutoAttacher::WorkerVersionDoomed( |
| ServiceWorkerDevToolsAgentHost* host) { |
| ReattachServiceWorkers(false); |
| } |
| |
| void TargetAutoAttacher::WorkerDestroyed(ServiceWorkerDevToolsAgentHost* host) { |
| ReattachServiceWorkers(false); |
| } |
| |
| void TargetAutoAttacher::ChildWorkerCreated(DevToolsAgentHostImpl* agent_host, |
| bool waiting_for_debugger) { |
| attach_callback_.Run(agent_host, waiting_for_debugger); |
| auto_attached_hosts_.insert(scoped_refptr<DevToolsAgentHost>(agent_host)); |
| } |
| |
| } // namespace protocol |
| } // namespace content |