// Copyright 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 "content/browser/devtools/protocol/service_worker_handler.h" #include "base/bind.h" #include "base/containers/scoped_ptr_hash_map.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "content/browser/devtools/service_worker_devtools_agent_host.h" #include "content/browser/devtools/service_worker_devtools_manager.h" #include "content/browser/frame_host/frame_tree.h" #include "content/browser/frame_host/frame_tree_node.h" #include "content/browser/frame_host/render_frame_host_impl.h" #include "content/browser/service_worker/service_worker_context_watcher.h" #include "content/browser/service_worker/service_worker_context_wrapper.h" #include "content/browser/service_worker/service_worker_version.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/storage_partition.h" #include "content/public/browser/web_contents.h" #include "content/public/common/push_messaging_status.h" #include "url/gurl.h" // Windows headers will redefine SendMessage. #ifdef SendMessage #undef SendMessage #endif namespace content { namespace devtools { namespace service_worker { using Response = DevToolsProtocolClient::Response; namespace { void ResultNoOp(bool success) { } void StatusNoOp(ServiceWorkerStatusCode status) { } void PushDeliveryNoOp(PushDeliveryStatus status) { } const std::string GetVersionRunningStatusString( content::ServiceWorkerVersion::RunningStatus running_status) { switch (running_status) { case content::ServiceWorkerVersion::STOPPED: return kServiceWorkerVersionRunningStatusStopped; case content::ServiceWorkerVersion::STARTING: return kServiceWorkerVersionRunningStatusStarting; case content::ServiceWorkerVersion::RUNNING: return kServiceWorkerVersionRunningStatusRunning; case content::ServiceWorkerVersion::STOPPING: return kServiceWorkerVersionRunningStatusStopping; } return std::string(); } const std::string GetVersionStatusString( content::ServiceWorkerVersion::Status status) { switch (status) { case content::ServiceWorkerVersion::NEW: return kServiceWorkerVersionStatusNew; case content::ServiceWorkerVersion::INSTALLING: return kServiceWorkerVersionStatusInstalling; case content::ServiceWorkerVersion::INSTALLED: return kServiceWorkerVersionStatusInstalled; case content::ServiceWorkerVersion::ACTIVATING: return kServiceWorkerVersionStatusActivating; case content::ServiceWorkerVersion::ACTIVATED: return kServiceWorkerVersionStatusActivated; case content::ServiceWorkerVersion::REDUNDANT: return kServiceWorkerVersionStatusRedundant; } return std::string(); } scoped_refptr CreateVersionDictionaryValue( const ServiceWorkerVersionInfo& version_info) { std::vector clients; for (const auto& client : version_info.clients) { if (client.second.type == SERVICE_WORKER_PROVIDER_FOR_WINDOW) { RenderFrameHostImpl* render_frame_host = RenderFrameHostImpl::FromID( client.second.process_id, client.second.route_id); WebContents* web_contents = WebContents::FromRenderFrameHost(render_frame_host); // There is a possibility that the frame is already deleted because of the // thread hopping. if (!web_contents) continue; scoped_refptr agent_host( DevToolsAgentHost::GetOrCreateFor(web_contents)); if (agent_host) clients.push_back(agent_host->GetId()); } else if (client.second.type == SERVICE_WORKER_PROVIDER_FOR_SHARED_WORKER) { scoped_refptr agent_host( DevToolsAgentHost::GetForWorker(client.second.process_id, client.second.route_id)); if (agent_host) clients.push_back(agent_host->GetId()); } } scoped_refptr version( ServiceWorkerVersion::Create() ->set_version_id(base::Int64ToString(version_info.version_id)) ->set_registration_id( base::Int64ToString(version_info.registration_id)) ->set_script_url(version_info.script_url.spec()) ->set_running_status( GetVersionRunningStatusString(version_info.running_status)) ->set_status(GetVersionStatusString(version_info.status)) ->set_script_last_modified( version_info.script_last_modified.ToDoubleT()) ->set_script_response_time( version_info.script_response_time.ToDoubleT()) ->set_controlled_clients(clients)); return version; } scoped_refptr CreateRegistrationDictionaryValue( const ServiceWorkerRegistrationInfo& registration_info) { scoped_refptr registration( ServiceWorkerRegistration::Create() ->set_registration_id( base::Int64ToString(registration_info.registration_id)) ->set_scope_url(registration_info.pattern.spec()) ->set_is_deleted(registration_info.delete_flag == ServiceWorkerRegistrationInfo::IS_DELETED)); return registration; } scoped_refptr GetMatchingServiceWorker( const ServiceWorkerDevToolsAgentHost::List& agent_hosts, const GURL& url) { scoped_refptr best_host; std::string best_scope; for (auto host : agent_hosts) { if (host->GetURL().host() != url.host()) continue; std::string path = host->GetURL().path(); std::string file = host->GetURL().ExtractFileName(); std::string scope = path.substr(0, path.length() - file.length()); if (scope.length() > best_scope.length()) { best_host = host; best_scope = scope; } } return best_host; } ServiceWorkerDevToolsAgentHost::Map GetMatchingServiceWorkers( BrowserContext* browser_context, const std::set& urls) { ServiceWorkerDevToolsAgentHost::Map result; if (!browser_context) return result; ServiceWorkerDevToolsAgentHost::List agent_hosts; ServiceWorkerDevToolsManager::GetInstance() ->AddAllAgentHostsForBrowserContext(browser_context, &agent_hosts); for (const GURL& url : urls) { scoped_refptr host = GetMatchingServiceWorker(agent_hosts, url); if (host) result[host->GetId()] = host; } return result; } bool CollectURLs(std::set* urls, FrameTreeNode* tree_node) { urls->insert(tree_node->current_url()); return false; } void StopServiceWorkerOnIO(scoped_refptr context, int64 version_id) { if (content::ServiceWorkerVersion* version = context->GetLiveVersion(version_id)) { version->StopWorker(base::Bind(&StatusNoOp)); } } void GetDevToolsRouteInfoOnIO( scoped_refptr context, int64 version_id, const base::Callback& callback) { if (content::ServiceWorkerVersion* version = context->GetLiveVersion(version_id)) { BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind( callback, version->embedded_worker()->process_id(), version->embedded_worker()->worker_devtools_agent_route_id())); } } Response CreateContextErrorResponse() { return Response::InternalError("Could not connect to the context"); } Response CreateInvalidVersionIdErrorResponse() { return Response::InternalError("Invalid version ID"); } const std::string GetDevToolsAgentHostTypeString( content::DevToolsAgentHost::Type type) { switch (type) { case DevToolsAgentHost::TYPE_WEB_CONTENTS: return "web_contents"; case DevToolsAgentHost::TYPE_FRAME: return "frame"; case DevToolsAgentHost::TYPE_SHARED_WORKER: return "shared_worker"; case DevToolsAgentHost::TYPE_SERVICE_WORKER: return "service_worker"; case DevToolsAgentHost::TYPE_EXTERNAL: return "external"; case DevToolsAgentHost::TYPE_BROWSER: return "browser"; } NOTREACHED() << type; return std::string(); } } // namespace ServiceWorkerHandler::ServiceWorkerHandler() : enabled_(false), render_frame_host_(nullptr), weak_factory_(this) { } ServiceWorkerHandler::~ServiceWorkerHandler() { Disable(); } void ServiceWorkerHandler::SetRenderFrameHost( RenderFrameHostImpl* render_frame_host) { render_frame_host_ = render_frame_host; // Do not call UpdateHosts yet, wait for load to commit. if (!render_frame_host) { context_ = nullptr; return; } StoragePartition* partition = BrowserContext::GetStoragePartition( render_frame_host->GetProcess()->GetBrowserContext(), render_frame_host->GetSiteInstance()); DCHECK(partition); context_ = static_cast( partition->GetServiceWorkerContext()); } void ServiceWorkerHandler::SetClient(scoped_ptr client) { client_.swap(client); } void ServiceWorkerHandler::UpdateHosts() { if (!enabled_) return; urls_.clear(); BrowserContext* browser_context = nullptr; if (render_frame_host_) { render_frame_host_->frame_tree_node()->frame_tree()->ForEach( base::Bind(&CollectURLs, &urls_)); browser_context = render_frame_host_->GetProcess()->GetBrowserContext(); } ServiceWorkerDevToolsAgentHost::Map old_hosts = attached_hosts_; ServiceWorkerDevToolsAgentHost::Map new_hosts = GetMatchingServiceWorkers(browser_context, urls_); for (auto pair : old_hosts) { if (new_hosts.find(pair.first) == new_hosts.end()) ReportWorkerTerminated(pair.second.get()); } for (auto pair : new_hosts) { if (old_hosts.find(pair.first) == old_hosts.end()) ReportWorkerCreated(pair.second.get()); } } void ServiceWorkerHandler::Detached() { Disable(); } Response ServiceWorkerHandler::Enable() { if (enabled_) return Response::OK(); if (!context_) return Response::InternalError("Could not connect to the context"); enabled_ = true; ServiceWorkerDevToolsManager::GetInstance()->AddObserver(this); client_->DebugOnStartUpdated( DebugOnStartUpdatedParams::Create()->set_debug_on_start( ServiceWorkerDevToolsManager::GetInstance() ->debug_service_worker_on_start())); context_watcher_ = new ServiceWorkerContextWatcher( context_, base::Bind(&ServiceWorkerHandler::OnWorkerRegistrationUpdated, weak_factory_.GetWeakPtr()), base::Bind(&ServiceWorkerHandler::OnWorkerVersionUpdated, weak_factory_.GetWeakPtr()), base::Bind(&ServiceWorkerHandler::OnErrorReported, weak_factory_.GetWeakPtr())); context_watcher_->Start(); UpdateHosts(); return Response::OK(); } Response ServiceWorkerHandler::Disable() { if (!enabled_) return Response::OK(); enabled_ = false; ServiceWorkerDevToolsManager::GetInstance()->RemoveObserver(this); for (const auto& pair : attached_hosts_) pair.second->DetachClient(); attached_hosts_.clear(); DCHECK(context_watcher_); context_watcher_->Stop(); context_watcher_ = nullptr; return Response::OK(); } Response ServiceWorkerHandler::SendMessage( const std::string& worker_id, const std::string& message) { auto it = attached_hosts_.find(worker_id); if (it == attached_hosts_.end()) return Response::InternalError("Not connected to the worker"); it->second->DispatchProtocolMessage(message); return Response::OK(); } Response ServiceWorkerHandler::Stop( const std::string& worker_id) { auto it = attached_hosts_.find(worker_id); if (it == attached_hosts_.end()) return Response::InternalError("Not connected to the worker"); it->second->UnregisterWorker(); return Response::OK(); } Response ServiceWorkerHandler::Unregister(const std::string& scope_url) { if (!enabled_) return Response::OK(); if (!context_) return CreateContextErrorResponse(); context_->UnregisterServiceWorker(GURL(scope_url), base::Bind(&ResultNoOp)); return Response::OK(); } Response ServiceWorkerHandler::StartWorker(const std::string& scope_url) { if (!enabled_) return Response::OK(); if (!context_) return CreateContextErrorResponse(); context_->StartServiceWorker(GURL(scope_url), base::Bind(&StatusNoOp)); return Response::OK(); } Response ServiceWorkerHandler::StopWorker(const std::string& version_id) { if (!enabled_) return Response::OK(); if (!context_) return CreateContextErrorResponse(); int64 id = 0; if (!base::StringToInt64(version_id, &id)) return CreateInvalidVersionIdErrorResponse(); BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(&StopServiceWorkerOnIO, context_, id)); return Response::OK(); } Response ServiceWorkerHandler::UpdateRegistration( const std::string& scope_url) { if (!enabled_) return Response::OK(); if (!context_) return CreateContextErrorResponse(); context_->UpdateRegistration(GURL(scope_url)); return Response::OK(); } Response ServiceWorkerHandler::InspectWorker(const std::string& version_id) { if (!enabled_) return Response::OK(); if (!context_) return CreateContextErrorResponse(); int64 id = 0; if (!base::StringToInt64(version_id, &id)) return CreateInvalidVersionIdErrorResponse(); BrowserThread::PostTask( BrowserThread::IO, FROM_HERE, base::Bind(&GetDevToolsRouteInfoOnIO, context_, id, base::Bind(&ServiceWorkerHandler::OpenNewDevToolsWindow, weak_factory_.GetWeakPtr()))); return Response::OK(); } Response ServiceWorkerHandler::SkipWaiting(const std::string& version_id) { if (!enabled_) return Response::OK(); if (!context_) return CreateContextErrorResponse(); int64 id = 0; if (!base::StringToInt64(version_id, &id)) return CreateInvalidVersionIdErrorResponse(); context_->SimulateSkipWaiting(id); return Response::OK(); } Response ServiceWorkerHandler::SetDebugOnStart(bool debug_on_start) { ServiceWorkerDevToolsManager::GetInstance() ->set_debug_service_worker_on_start(debug_on_start); return Response::OK(); } Response ServiceWorkerHandler::DeliverPushMessage( const std::string& origin, const std::string& registration_id, const std::string& data) { if (!enabled_) return Response::OK(); if (!render_frame_host_) return CreateContextErrorResponse(); int64 id = 0; if (!base::StringToInt64(registration_id, &id)) return CreateInvalidVersionIdErrorResponse(); BrowserContext::DeliverPushMessage( render_frame_host_->GetProcess()->GetBrowserContext(), GURL(origin), id, data, base::Bind(&PushDeliveryNoOp)); return Response::OK(); } Response ServiceWorkerHandler::GetTargetInfo( const std::string& target_id, scoped_refptr* target_info) { scoped_refptr agent_host( DevToolsAgentHost::GetForId(target_id)); if (!agent_host) return Response::InvalidParams("targetId"); *target_info = TargetInfo::Create() ->set_id(agent_host->GetId()) ->set_type(GetDevToolsAgentHostTypeString(agent_host->GetType())) ->set_title(agent_host->GetTitle()) ->set_url(agent_host->GetURL().spec()); return Response::OK(); } Response ServiceWorkerHandler::ActivateTarget(const std::string& target_id) { scoped_refptr agent_host( DevToolsAgentHost::GetForId(target_id)); if (!agent_host) return Response::InvalidParams("targetId"); agent_host->Activate(); return Response::OK(); } void ServiceWorkerHandler::OpenNewDevToolsWindow(int process_id, int devtools_agent_route_id) { scoped_refptr agent_host( ServiceWorkerDevToolsManager::GetInstance() ->GetDevToolsAgentHostForWorker(process_id, devtools_agent_route_id)); if (!agent_host.get()) return; agent_host->Inspect(render_frame_host_->GetProcess()->GetBrowserContext()); } void ServiceWorkerHandler::OnWorkerRegistrationUpdated( const std::vector& registrations) { std::vector> registration_values; for (const auto& registration : registrations) { registration_values.push_back( CreateRegistrationDictionaryValue(registration)); } client_->WorkerRegistrationUpdated( WorkerRegistrationUpdatedParams::Create()->set_registrations( registration_values)); } void ServiceWorkerHandler::OnWorkerVersionUpdated( const std::vector& versions) { std::vector> version_values; for (const auto& version : versions) { version_values.push_back(CreateVersionDictionaryValue(version)); } client_->WorkerVersionUpdated( WorkerVersionUpdatedParams::Create()->set_versions(version_values)); } void ServiceWorkerHandler::OnErrorReported( int64 registration_id, int64 version_id, const ServiceWorkerContextObserver::ErrorInfo& info) { client_->WorkerErrorReported( WorkerErrorReportedParams::Create()->set_error_message( ServiceWorkerErrorMessage::Create() ->set_error_message(base::UTF16ToUTF8(info.error_message)) ->set_registration_id(base::Int64ToString(registration_id)) ->set_version_id(base::Int64ToString(version_id)) ->set_source_url(info.source_url.spec()) ->set_line_number(info.line_number) ->set_column_number(info.column_number))); } void ServiceWorkerHandler::DispatchProtocolMessage( DevToolsAgentHost* host, const std::string& message) { auto it = attached_hosts_.find(host->GetId()); if (it == attached_hosts_.end()) return; // Already disconnected. client_->DispatchMessage( DispatchMessageParams::Create()-> set_worker_id(host->GetId())-> set_message(message)); } void ServiceWorkerHandler::AgentHostClosed( DevToolsAgentHost* host, bool replaced_with_another_client) { client_->WorkerTerminated(WorkerTerminatedParams::Create()-> set_worker_id(host->GetId())); attached_hosts_.erase(host->GetId()); } void ServiceWorkerHandler::WorkerCreated( ServiceWorkerDevToolsAgentHost* host) { BrowserContext* browser_context = nullptr; if (render_frame_host_) browser_context = render_frame_host_->GetProcess()->GetBrowserContext(); auto hosts = GetMatchingServiceWorkers(browser_context, urls_); if (hosts.find(host->GetId()) != hosts.end() && !host->IsAttached() && !host->IsPausedForDebugOnStart()) host->PauseForDebugOnStart(); } void ServiceWorkerHandler::WorkerReadyForInspection( ServiceWorkerDevToolsAgentHost* host) { if (ServiceWorkerDevToolsManager::GetInstance() ->debug_service_worker_on_start()) { // When debug_service_worker_on_start is true, a new DevTools window will // be opend in ServiceWorkerDevToolsManager::WorkerReadyForInspection. return; } UpdateHosts(); } void ServiceWorkerHandler::WorkerDestroyed( ServiceWorkerDevToolsAgentHost* host) { UpdateHosts(); } void ServiceWorkerHandler::DebugOnStartUpdated(bool debug_on_start) { client_->DebugOnStartUpdated( DebugOnStartUpdatedParams::Create()->set_debug_on_start(debug_on_start)); } void ServiceWorkerHandler::ReportWorkerCreated( ServiceWorkerDevToolsAgentHost* host) { if (host->IsAttached()) return; attached_hosts_[host->GetId()] = host; host->AttachClient(this); client_->WorkerCreated(WorkerCreatedParams::Create()-> set_worker_id(host->GetId())-> set_url(host->GetURL().spec())); } void ServiceWorkerHandler::ReportWorkerTerminated( ServiceWorkerDevToolsAgentHost* host) { auto it = attached_hosts_.find(host->GetId()); if (it == attached_hosts_.end()) return; host->DetachClient(); client_->WorkerTerminated(WorkerTerminatedParams::Create()-> set_worker_id(host->GetId())); attached_hosts_.erase(it); } } // namespace service_worker } // namespace devtools } // namespace content