| // 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/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() { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| static base::NoDestructor<ServiceWorkerDevToolsManager> instance; | |
| return &*instance; | |
| } | |
| ServiceWorkerDevToolsAgentHost* | |
| ServiceWorkerDevToolsManager::GetDevToolsAgentHostForWorker( | |
| ChildProcessId 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()); | |
| for (auto& it : new_installing_hosts_) { | |
| result->push_back(it.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) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| // 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); | |
| CHECK(!agent_host, base::NotFatalUntil::M159); | |
| scoped_refptr<ServiceWorkerDevToolsAgentHost> host = | |
| base::MakeRefCounted<ServiceWorkerDevToolsAgentHost>( | |
| ChildProcessId(), -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) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| 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( | |
| ChildProcessId 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) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| const WorkerId worker_id(worker_process_id, worker_route_id); | |
| CHECK(!live_hosts_.contains(worker_id), base::NotFatalUntil::M159); | |
| 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( | |
| ChildProcessId worker_process_id, | |
| int worker_route_id, | |
| mojo::PendingRemote<blink::mojom::DevToolsAgent> agent_remote, | |
| mojo::PendingReceiver<blink::mojom::DevToolsAgentHost> host_receiver) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| 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( | |
| ChildProcessId worker_process_id, | |
| int worker_route_id) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| 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( | |
| ChildProcessId worker_process_id, | |
| int worker_route_id, | |
| scoped_refptr<ServiceWorkerContextWrapper> context_wrapper, | |
| int64_t version_id) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| 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( | |
| ChildProcessId worker_process_id, | |
| int worker_route_id) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| 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) { | |
| CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M159); | |
| // 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( | |
| ChildProcessId 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( | |
| ChildProcessId 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( | |
| ChildProcessId 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 |