|  | // Copyright 2014 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "content/browser/devtools/service_worker_devtools_manager.h" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <optional> | 
|  |  | 
|  | #include "base/containers/contains.h" | 
|  | #include "base/no_destructor.h" | 
|  | #include "base/observer_list.h" | 
|  | #include "content/browser/devtools/devtools_instrumentation.h" | 
|  | #include "content/browser/devtools/protocol/network_handler.h" | 
|  | #include "content/browser/devtools/protocol/page_handler.h" | 
|  | #include "content/browser/devtools/service_worker_devtools_agent_host.h" | 
|  | #include "content/browser/service_worker/service_worker_context_wrapper.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "content/public/browser/render_process_host.h" | 
|  | #include "services/network/public/cpp/devtools_observer_util.h" | 
|  | #include "services/network/public/mojom/devtools_observer.mojom.h" | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | // static | 
|  | ServiceWorkerDevToolsManager* ServiceWorkerDevToolsManager::GetInstance() { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | static base::NoDestructor<ServiceWorkerDevToolsManager> instance; | 
|  | return &*instance; | 
|  | } | 
|  |  | 
|  | ServiceWorkerDevToolsAgentHost* | 
|  | ServiceWorkerDevToolsManager::GetDevToolsAgentHostForWorker( | 
|  | int worker_process_id, | 
|  | int worker_route_id) { | 
|  | auto it = live_hosts_.find(WorkerId(worker_process_id, worker_route_id)); | 
|  | return it == live_hosts_.end() ? nullptr : it->second.get(); | 
|  | } | 
|  |  | 
|  | ServiceWorkerDevToolsAgentHost* | 
|  | ServiceWorkerDevToolsManager::GetDevToolsAgentHostForNewInstallingWorker( | 
|  | const ServiceWorkerContextWrapper* context_wrapper, | 
|  | int64_t version_id) { | 
|  | auto it = std::ranges::find_if( | 
|  | new_installing_hosts_, | 
|  | [&context_wrapper, &version_id]( | 
|  | const scoped_refptr<ServiceWorkerDevToolsAgentHost>& agent_host) { | 
|  | return agent_host->context_wrapper() == context_wrapper && | 
|  | agent_host->version_id() == version_id; | 
|  | }); | 
|  | if (it == new_installing_hosts_.end()) | 
|  | return nullptr; | 
|  | return it->get(); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::AddAllAgentHosts( | 
|  | ServiceWorkerDevToolsAgentHost::List* result) { | 
|  | for (auto& it : live_hosts_) | 
|  | result->push_back(it.second.get()); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::AddAllAgentHostsForBrowserContext( | 
|  | BrowserContext* browser_context, | 
|  | ServiceWorkerDevToolsAgentHost::List* result) { | 
|  | for (auto& it : live_hosts_) { | 
|  | if (it.second->GetBrowserContext() == browser_context) | 
|  | result->push_back(it.second.get()); | 
|  | } | 
|  | for (auto& it : new_installing_hosts_) { | 
|  | if (it->GetBrowserContext() == browser_context) | 
|  | result->push_back(it.get()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::WorkerMainScriptFetchingStarting( | 
|  | scoped_refptr<ServiceWorkerContextWrapper> context_wrapper, | 
|  | int64_t version_id, | 
|  | const GURL& url, | 
|  | const GURL& scope, | 
|  | const GlobalRenderFrameHostId& requesting_frame_id, | 
|  | scoped_refptr<DevToolsThrottleHandle> throttle_handle) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  |  | 
|  | // Verify that we are not getting a similar host that's already in a stopped | 
|  | // state. This should never happen, we are installing a new SW, we cannot | 
|  | // have the same one that was started and stopped. | 
|  | ServiceWorkerContextWrapper* context_wrapper_ptr = context_wrapper.get(); | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> agent_host = | 
|  | TakeStoppedHost(context_wrapper_ptr, version_id); | 
|  | DCHECK(!agent_host); | 
|  |  | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> host = | 
|  | base::MakeRefCounted<ServiceWorkerDevToolsAgentHost>( | 
|  | -1, -1, std::move(context_wrapper), version_id, url, scope, | 
|  | /*is_installed_version=*/false, | 
|  | /*client_security_state=*/nullptr, | 
|  | /*coep_reporter=*/mojo::NullRemote(), | 
|  | /*dip_reporter=*/mojo::NullRemote(), | 
|  | base::UnguessableToken::Create()); | 
|  |  | 
|  | ServiceWorkerDevToolsAgentHost* host_ptr = host.get(); | 
|  | new_installing_hosts_.insert(std::move(host)); | 
|  |  | 
|  | for (auto& observer : observer_list_) { | 
|  | bool should_pause_on_start = false; | 
|  | observer.WorkerCreated(host_ptr, &should_pause_on_start); | 
|  | if (should_pause_on_start) { | 
|  | host_ptr->set_should_pause_on_start(true); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Now that we have a devtools target, we need to give devtools the | 
|  | // opportunity to attach to it before we do the actual fetch. We pass it a | 
|  | // callback that will be called once we have all the handlers ready. | 
|  | if (host_ptr->should_pause_on_start()) { | 
|  | devtools_instrumentation::ThrottleServiceWorkerMainScriptFetch( | 
|  | context_wrapper_ptr, version_id, requesting_frame_id, throttle_handle); | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::WorkerMainScriptFetchingFailed( | 
|  | scoped_refptr<ServiceWorkerContextWrapper> context_wrapper, | 
|  | int64_t version_id) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> host = | 
|  | TakeNewInstallingHost(context_wrapper.get(), version_id); | 
|  |  | 
|  | // While not strictly required, some WPTs expect all messages to be answered | 
|  | // before finishing and will loop until they get an answer. This call makes | 
|  | // sure all pending messages are answered with an error when we fail the | 
|  | // main script fetch. | 
|  | host->WorkerMainScriptFetchingFailed(); | 
|  |  | 
|  | // This observer call should trigger the destruction of the | 
|  | // ServiceWorkerDevToolsAgentHost by removing the scoped_ptr references held | 
|  | // by auto-attachers. | 
|  | for (auto& observer : observer_list_) | 
|  | observer.WorkerDestroyed(host.get()); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::WorkerStarting( | 
|  | int worker_process_id, | 
|  | int worker_route_id, | 
|  | scoped_refptr<ServiceWorkerContextWrapper> context_wrapper, | 
|  | int64_t version_id, | 
|  | const GURL& url, | 
|  | const GURL& scope, | 
|  | bool is_installed_version, | 
|  | network::mojom::ClientSecurityStatePtr client_security_state, | 
|  | mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter> | 
|  | coep_reporter, | 
|  | mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter> | 
|  | dip_reporter, | 
|  | base::UnguessableToken* devtools_worker_token, | 
|  | bool* pause_on_start) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | DCHECK(!base::Contains(live_hosts_, worker_id)); | 
|  |  | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> agent_host = | 
|  | TakeStoppedHost(context_wrapper.get(), version_id); | 
|  | if (agent_host) { | 
|  | live_hosts_[worker_id] = agent_host; | 
|  | agent_host->WorkerStarted(worker_process_id, worker_route_id); | 
|  | *pause_on_start = | 
|  | agent_host->IsAttached() && agent_host->should_pause_on_start(); | 
|  | *devtools_worker_token = agent_host->devtools_worker_token(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | agent_host = TakeNewInstallingHost(context_wrapper.get(), version_id); | 
|  | if (agent_host) { | 
|  | live_hosts_[worker_id] = agent_host; | 
|  | agent_host->WorkerStarted(worker_process_id, worker_route_id); | 
|  | *pause_on_start = agent_host->should_pause_on_start(); | 
|  | *devtools_worker_token = agent_host->devtools_worker_token(); | 
|  |  | 
|  | if (client_security_state) { | 
|  | agent_host->UpdateClientSecurityState(std::move(client_security_state), | 
|  | std::move(coep_reporter), | 
|  | std::move(dip_reporter)); | 
|  | } | 
|  |  | 
|  | return; | 
|  | } | 
|  |  | 
|  | *devtools_worker_token = base::UnguessableToken::Create(); | 
|  | auto host = base::MakeRefCounted<ServiceWorkerDevToolsAgentHost>( | 
|  | worker_process_id, worker_route_id, std::move(context_wrapper), | 
|  | version_id, url, scope, is_installed_version, | 
|  | std::move(client_security_state), std::move(coep_reporter), | 
|  | std::move(dip_reporter), *devtools_worker_token); | 
|  | live_hosts_[worker_id] = host; | 
|  | *pause_on_start = debug_service_worker_on_start_; | 
|  | for (auto& observer : observer_list_) { | 
|  | bool should_pause_on_start = false; | 
|  | observer.WorkerCreated(host.get(), &should_pause_on_start); | 
|  | if (should_pause_on_start) | 
|  | *pause_on_start = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::WorkerReadyForInspection( | 
|  | int worker_process_id, | 
|  | int worker_route_id, | 
|  | mojo::PendingRemote<blink::mojom::DevToolsAgent> agent_remote, | 
|  | mojo::PendingReceiver<blink::mojom::DevToolsAgentHost> host_receiver) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | auto it = live_hosts_.find(worker_id); | 
|  | if (it == live_hosts_.end()) | 
|  | return; | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> host = it->second; | 
|  | host->WorkerReadyForInspection(std::move(agent_remote), | 
|  | std::move(host_receiver)); | 
|  | // Bring up UI for the ones not picked by other clients. | 
|  | if (debug_service_worker_on_start_ && !host->IsAttached()) | 
|  | host->Inspect(); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::WorkerVersionInstalled(int worker_process_id, | 
|  | int worker_route_id) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | auto it = live_hosts_.find(worker_id); | 
|  | if (it == live_hosts_.end()) | 
|  | return; | 
|  | it->second->WorkerVersionInstalled(); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::WorkerVersionDoomed( | 
|  | int worker_process_id, | 
|  | int worker_route_id, | 
|  | scoped_refptr<ServiceWorkerContextWrapper> context_wrapper, | 
|  | int64_t version_id) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | auto it = live_hosts_.find(worker_id); | 
|  | if (it != live_hosts_.end()) { | 
|  | it->second->WorkerVersionDoomed(); | 
|  | return; | 
|  | } | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> host = | 
|  | TakeStoppedHost(context_wrapper.get(), version_id); | 
|  | if (!host) | 
|  | return; | 
|  | host->WorkerVersionDoomed(); | 
|  | // The worker has already been stopped and since it's doomed it will never | 
|  | // restart. | 
|  | for (auto& observer : observer_list_) | 
|  | observer.WorkerDestroyed(host.get()); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::WorkerStopped(int worker_process_id, | 
|  | int worker_route_id) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | auto it = live_hosts_.find(worker_id); | 
|  | if (it == live_hosts_.end()) | 
|  | return; | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> agent_host(it->second); | 
|  | live_hosts_.erase(it); | 
|  | agent_host->WorkerStopped(); | 
|  | if (agent_host->version_doomed_time().is_null()) { | 
|  | stopped_hosts_.insert(agent_host.get()); | 
|  | } else { | 
|  | // The worker version has been doomed, it will never restart. | 
|  | for (auto& observer : observer_list_) | 
|  | observer.WorkerDestroyed(agent_host.get()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::AgentHostDestroyed( | 
|  | ServiceWorkerDevToolsAgentHost* agent_host) { | 
|  | DCHECK_CURRENTLY_ON(BrowserThread::UI); | 
|  | // Might be missing during shutdown due to different | 
|  | // destruction order of this manager, service workers | 
|  | // and their agent hosts. | 
|  | stopped_hosts_.erase(agent_host); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::AddObserver(Observer* observer) { | 
|  | observer_list_.AddObserver(observer); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::RemoveObserver(Observer* observer) { | 
|  | observer_list_.RemoveObserver(observer); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::set_debug_service_worker_on_start( | 
|  | bool debug_on_start) { | 
|  | debug_service_worker_on_start_ = debug_on_start; | 
|  | } | 
|  |  | 
|  | ServiceWorkerDevToolsManager::ServiceWorkerDevToolsManager() | 
|  | : debug_service_worker_on_start_(false) { | 
|  | } | 
|  |  | 
|  | ServiceWorkerDevToolsManager::~ServiceWorkerDevToolsManager() = default; | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::NavigationPreloadRequestSent( | 
|  | int worker_process_id, | 
|  | int worker_route_id, | 
|  | const std::string& request_id, | 
|  | const network::ResourceRequest& request) { | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | auto it = live_hosts_.find(worker_id); | 
|  | if (it == live_hosts_.end()) | 
|  | return; | 
|  | auto timestamp = base::TimeTicks::Now(); | 
|  | network::mojom::URLRequestDevToolsInfoPtr request_info = | 
|  | network::ExtractDevToolsInfo(request); | 
|  | for (auto* network : | 
|  | protocol::NetworkHandler::ForAgentHost(it->second.get())) { | 
|  | network->RequestSent(request_id, std::string(), request.headers, | 
|  | *request_info, | 
|  | protocol::Network::Initiator::TypeEnum::Preload, | 
|  | /*initiator_url=*/std::nullopt, | 
|  | /*initiator_devtools_request_id=*/"", | 
|  | /*frame_token=*/std::nullopt, timestamp); | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::NavigationPreloadResponseReceived( | 
|  | int worker_process_id, | 
|  | int worker_route_id, | 
|  | const std::string& request_id, | 
|  | const GURL& url, | 
|  | const network::mojom::URLResponseHead& head) { | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | auto it = live_hosts_.find(worker_id); | 
|  | if (it == live_hosts_.end()) | 
|  | return; | 
|  |  | 
|  | network::mojom::URLResponseHeadDevToolsInfoPtr head_info = | 
|  | network::ExtractDevToolsInfo(head); | 
|  | for (auto* network : protocol::NetworkHandler::ForAgentHost(it->second.get())) | 
|  | network->ResponseReceived(request_id, std::string(), url, | 
|  | protocol::Network::ResourceTypeEnum::Other, | 
|  | *head_info, std::nullopt); | 
|  | } | 
|  |  | 
|  | void ServiceWorkerDevToolsManager::NavigationPreloadCompleted( | 
|  | int worker_process_id, | 
|  | int worker_route_id, | 
|  | const std::string& request_id, | 
|  | const network::URLLoaderCompletionStatus& status) { | 
|  | const WorkerId worker_id(worker_process_id, worker_route_id); | 
|  | auto it = live_hosts_.find(worker_id); | 
|  | if (it == live_hosts_.end()) | 
|  | return; | 
|  | for (auto* network : protocol::NetworkHandler::ForAgentHost(it->second.get())) | 
|  | network->LoadingComplete( | 
|  | request_id, protocol::Network::ResourceTypeEnum::Other, status); | 
|  | } | 
|  |  | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> | 
|  | ServiceWorkerDevToolsManager::TakeStoppedHost( | 
|  | const ServiceWorkerContextWrapper* context_wrapper, | 
|  | int64_t version_id) { | 
|  | auto it = std::ranges::find_if( | 
|  | stopped_hosts_, [&context_wrapper, &version_id]( | 
|  | ServiceWorkerDevToolsAgentHost* agent_host) { | 
|  | return agent_host->context_wrapper() == context_wrapper && | 
|  | agent_host->version_id() == version_id; | 
|  | }); | 
|  | if (it == stopped_hosts_.end()) | 
|  | return nullptr; | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> agent_host(*it); | 
|  | stopped_hosts_.erase(it); | 
|  | return agent_host; | 
|  | } | 
|  |  | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> | 
|  | ServiceWorkerDevToolsManager::TakeNewInstallingHost( | 
|  | const ServiceWorkerContextWrapper* context_wrapper, | 
|  | int64_t version_id) { | 
|  | auto it = std::ranges::find_if( | 
|  | new_installing_hosts_, | 
|  | [&context_wrapper, &version_id]( | 
|  | const scoped_refptr<ServiceWorkerDevToolsAgentHost>& agent_host) { | 
|  | return agent_host->context_wrapper() == context_wrapper && | 
|  | agent_host->version_id() == version_id; | 
|  | }); | 
|  | if (it == new_installing_hosts_.end()) | 
|  | return nullptr; | 
|  | scoped_refptr<ServiceWorkerDevToolsAgentHost> agent_host(std::move(*it)); | 
|  | new_installing_hosts_.erase(it); | 
|  | return agent_host; | 
|  | } | 
|  |  | 
|  | }  // namespace content |