| // Copyright 2021 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/frame_auto_attacher.h" |
| |
| #include "base/time/time.h" |
| #include "content/browser/devtools/auction_worklet_devtools_agent_host.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/renderer_host/frame_tree.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| |
| namespace content { |
| |
| 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.DeprecatedGetOriginAsURL()); |
| for (const auto& host : agent_hosts) { |
| if (host_name_set.find(host->scope().DeprecatedGetOriginAsURL()) == |
| 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()->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 |
| |
| FrameAutoAttacher::FrameAutoAttacher(DevToolsRendererChannel* renderer_channel) |
| : RendererAutoAttacherBase(renderer_channel) {} |
| |
| FrameAutoAttacher::~FrameAutoAttacher() = default; |
| |
| void FrameAutoAttacher::SetRenderFrameHost( |
| RenderFrameHostImpl* render_frame_host) { |
| render_frame_host_ = render_frame_host; |
| if (!auto_attach()) |
| return; |
| UpdateFrames(); |
| UpdatePages(); |
| ReattachServiceWorkers(); |
| } |
| |
| void FrameAutoAttacher::DidFinishNavigation( |
| NavigationRequest* navigation_request) { |
| if (!render_frame_host_) |
| return; |
| |
| if (navigation_request->frame_tree_node() == |
| render_frame_host_->frame_tree_node()) { |
| ReattachServiceWorkers(); |
| return; |
| } |
| |
| // We only care about subframes that have |render_frame_host_| as their |
| // local root. |
| if (!navigation_request->HasCommitted()) |
| return; |
| RenderFrameHostImpl* parent = navigation_request->GetParentFrame(); |
| while (parent && !parent->is_local_root()) |
| parent = parent->GetParent(); |
| if (parent != render_frame_host_) |
| return; |
| |
| // Some subframes may not be attached through |
| // TargetHandler::ResponseThrottle because DevTools wasn't attached when the |
| // navigation started, so no throttle was installed. We auto-attach them |
| // here instead (note that we cannot honor |wait_for_debugger_on_start_| in |
| // this case). |
| AutoAttachToFrame(navigation_request, false); |
| } |
| |
| void FrameAutoAttacher::UpdatePages() { |
| if (!auto_attach()) |
| return; |
| |
| Hosts new_hosts; |
| if (render_frame_host_) { |
| render_frame_host_->ForEachRenderFrameHost(base::BindRepeating( |
| [](Hosts* new_hosts, RenderFrameHost* root, RenderFrameHost* rfh) { |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(rfh); |
| if (rfh == root) |
| return RenderFrameHost::FrameIterationAction::kContinue; |
| |
| FrameTreeNode* frame_tree_node = rfhi->frame_tree_node(); |
| if (frame_tree_node->IsMainFrame() && |
| (frame_tree_node->IsFencedFrameRoot() || |
| WebContentsImpl::FromFrameTreeNode(frame_tree_node) |
| ->IsPortal())) { |
| scoped_refptr<DevToolsAgentHost> new_host = |
| RenderFrameDevToolsAgentHost::GetOrCreateFor(frame_tree_node); |
| new_hosts->insert(new_host); |
| return RenderFrameHost::FrameIterationAction::kSkipChildren; |
| } |
| |
| if (rfhi->is_local_root()) |
| return RenderFrameHost::FrameIterationAction::kSkipChildren; |
| |
| return RenderFrameHost::FrameIterationAction::kContinue; |
| }, |
| &new_hosts, render_frame_host_)); |
| } |
| |
| DispatchSetAttachedTargetsOfType(new_hosts, DevToolsAgentHost::kTypePage); |
| } |
| |
| void FrameAutoAttacher::AutoAttachToPage(FrameTree* frame_tree, |
| bool wait_for_debugger_on_start) { |
| if (!auto_attach()) |
| return; |
| scoped_refptr<DevToolsAgentHost> agent_host = |
| RenderFrameDevToolsAgentHost::GetOrCreateFor(frame_tree->root()); |
| DispatchAutoAttach(agent_host.get(), wait_for_debugger_on_start); |
| } |
| |
| void FrameAutoAttacher::UpdateAutoAttach(base::OnceClosure callback) { |
| if (auto_attach()) { |
| UpdateFrames(); |
| UpdatePages(); |
| if (render_frame_host_ && !render_frame_host_->GetParent() && |
| !observing_service_workers_) { |
| observing_service_workers_ = true; |
| ServiceWorkerDevToolsManager::GetInstance()->AddObserver(this); |
| } |
| if (observing_service_workers_) { |
| // Update service workers even if we've already been observing them, |
| // to notify new clients about existing service workers. |
| // This is similar to frames and pages above. |
| ReattachServiceWorkers(); |
| } |
| if (render_frame_host_ && !observing_auction_worklets_) { |
| observing_auction_worklets_ = true; |
| DebuggableAuctionWorkletTracker::GetInstance()->AddObserver(this); |
| } |
| } else { |
| if (observing_service_workers_) { |
| ServiceWorkerDevToolsManager::GetInstance()->RemoveObserver(this); |
| observing_service_workers_ = false; |
| } |
| if (observing_auction_worklets_) { |
| DebuggableAuctionWorkletTracker::GetInstance()->RemoveObserver(this); |
| observing_auction_worklets_ = false; |
| } |
| } |
| RendererAutoAttacherBase::UpdateAutoAttach(std::move(callback)); |
| } |
| |
| void FrameAutoAttacher::WorkerCreated(ServiceWorkerDevToolsAgentHost* host, |
| bool* should_pause_on_start) { |
| if (!render_frame_host_) |
| return; |
| BrowserContext* browser_context = |
| render_frame_host_->GetProcess()->GetBrowserContext(); |
| auto hosts = GetMatchingServiceWorkers(browser_context, |
| GetFrameUrls(render_frame_host_)); |
| if (hosts.find(host->GetId()) == hosts.end()) |
| return; |
| |
| *should_pause_on_start = wait_for_debugger_on_start(); |
| DispatchAutoAttach(host, *should_pause_on_start); |
| } |
| |
| void FrameAutoAttacher::WorkerDestroyed(ServiceWorkerDevToolsAgentHost* host) { |
| ReattachServiceWorkers(); |
| } |
| |
| void FrameAutoAttacher::AuctionWorkletCreated(DebuggableAuctionWorklet* worklet, |
| bool& should_pause_on_start) { |
| if (!render_frame_host_) |
| return; |
| if (!AuctionWorkletDevToolsAgentHost::IsRelevantTo(render_frame_host_, |
| worklet)) { |
| return; |
| } |
| should_pause_on_start = wait_for_debugger_on_start(); |
| DispatchAutoAttach(AuctionWorkletDevToolsAgentHostManager::GetInstance() |
| .GetOrCreateFor(worklet) |
| .get(), |
| should_pause_on_start); |
| } |
| |
| void FrameAutoAttacher::ReattachServiceWorkers() { |
| if (!observing_service_workers_ || !render_frame_host_) |
| return; |
| BrowserContext* 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); |
| DispatchSetAttachedTargetsOfType(new_hosts, |
| DevToolsAgentHost::kTypeServiceWorker); |
| } |
| |
| void FrameAutoAttacher::UpdateFrames() { |
| DCHECK(auto_attach()); |
| |
| Hosts new_hosts; |
| DevToolsAgentHost::List new_worklet_hosts; |
| if (render_frame_host_) { |
| base::queue<FrameTreeNode*> queue; |
| for (size_t i = 0; i < render_frame_host_->child_count(); ++i) { |
| queue.push(render_frame_host_->child_at(i)); |
| } |
| while (!queue.empty()) { |
| FrameTreeNode* node = queue.front(); |
| queue.pop(); |
| bool should_create = node->current_frame_host()->is_local_root_subframe(); |
| if (should_create) { |
| scoped_refptr<DevToolsAgentHost> new_host = |
| RenderFrameDevToolsAgentHost::GetOrCreateFor(node); |
| new_hosts.insert(new_host); |
| // Note: We don't add children of a local root to |queue|, as they |
| // will be looked at by a separate TargetAutoAttacher created for the |
| // local root. |
| } else { |
| for (size_t i = 0; i < node->child_count(); ++i) |
| queue.push(node->child_at(i)); |
| } |
| } |
| |
| AuctionWorkletDevToolsAgentHostManager::GetInstance().GetAllForFrame( |
| render_frame_host_, &new_worklet_hosts); |
| } |
| |
| DispatchSetAttachedTargetsOfType(new_hosts, DevToolsAgentHost::kTypeFrame); |
| DispatchSetAttachedTargetsOfType( |
| TargetAutoAttacher::Hosts(new_worklet_hosts.begin(), |
| new_worklet_hosts.end()), |
| DevToolsAgentHost::kTypeAuctionWorklet); |
| } |
| |
| } // namespace content |