blob: b9f528fdd5bf5d7cc2974b29f6fc683f280ab737 [file] [log] [blame]
// 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/background_sync/background_sync_context.h"
#include "content/browser/background_sync/background_sync_manager.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/embedded_worker_status.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/browser/storage_partition_impl_map.h"
#include "content/common/service_worker/service_worker_utils.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_event_payload.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 {
using ScopeAgentsMap =
std::map<GURL, std::unique_ptr<ServiceWorkerDevToolsAgentHost::List>>;
void ResultNoOp(bool success) {
}
void StatusNoOp(ServiceWorkerStatusCode status) {
}
void StatusNoOpKeepingRegistration(
scoped_refptr<content::ServiceWorkerRegistration> protect,
ServiceWorkerStatusCode status) {
}
void PushDeliveryNoOp(PushDeliveryStatus status) {
}
const std::string GetVersionRunningStatusString(
EmbeddedWorkerStatus running_status) {
switch (running_status) {
case EmbeddedWorkerStatus::STOPPED:
return kServiceWorkerVersionRunningStatusStopped;
case EmbeddedWorkerStatus::STARTING:
return kServiceWorkerVersionRunningStatusStarting;
case EmbeddedWorkerStatus::RUNNING:
return kServiceWorkerVersionRunningStatusRunning;
case EmbeddedWorkerStatus::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<ServiceWorkerVersion> CreateVersionDictionaryValue(
const ServiceWorkerVersionInfo& version_info) {
std::vector<std::string> 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<DevToolsAgentHost> 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<DevToolsAgentHost> agent_host(
DevToolsAgentHost::GetForWorker(client.second.process_id,
client.second.route_id));
if (agent_host)
clients.push_back(agent_host->GetId());
}
}
scoped_refptr<ServiceWorkerVersion> 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<ServiceWorkerRegistration> CreateRegistrationDictionaryValue(
const ServiceWorkerRegistrationInfo& registration_info) {
scoped_refptr<ServiceWorkerRegistration> 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;
}
void GetMatchingHostsByScopeMap(
const ServiceWorkerDevToolsAgentHost::List& agent_hosts,
const std::set<GURL>& urls,
ScopeAgentsMap* scope_agents_map) {
std::set<base::StringPiece> host_name_set;
for (const GURL& url : urls)
host_name_set.insert(url.host_piece());
for (const auto& host : agent_hosts) {
if (host_name_set.find(host->scope().host_piece()) == 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 std::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;
}
void StopServiceWorkerOnIO(scoped_refptr<ServiceWorkerContextWrapper> context,
int64_t version_id) {
if (content::ServiceWorkerVersion* version =
context->GetLiveVersion(version_id)) {
version->StopWorker(base::Bind(&StatusNoOp));
}
}
void GetDevToolsRouteInfoOnIO(
scoped_refptr<ServiceWorkerContextWrapper> context,
int64_t version_id,
const base::Callback<void(int, int)>& 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();
}
void DidFindRegistrationForDispatchSyncEventOnIO(
scoped_refptr<BackgroundSyncContext> sync_context,
const std::string& tag,
bool last_chance,
ServiceWorkerStatusCode status,
const scoped_refptr<content::ServiceWorkerRegistration>& registration) {
if (status != SERVICE_WORKER_OK || !registration->active_version())
return;
BackgroundSyncManager* background_sync_manager =
sync_context->background_sync_manager();
scoped_refptr<content::ServiceWorkerVersion> version(
registration->active_version());
// Keep the registration while dispatching the sync event.
background_sync_manager->EmulateDispatchSyncEvent(
tag, std::move(version), last_chance,
base::Bind(&StatusNoOpKeepingRegistration, registration));
}
void DispatchSyncEventOnIO(scoped_refptr<ServiceWorkerContextWrapper> context,
scoped_refptr<BackgroundSyncContext> sync_context,
const GURL& origin,
int64_t registration_id,
const std::string& tag,
bool last_chance) {
context->FindReadyRegistrationForId(
registration_id, origin,
base::Bind(&DidFindRegistrationForDispatchSyncEventOnIO, sync_context,
tag, last_chance));
}
} // 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) {
ClearForceUpdate();
context_ = nullptr;
return;
}
StoragePartition* partition = BrowserContext::GetStoragePartition(
render_frame_host->GetProcess()->GetBrowserContext(),
render_frame_host->GetSiteInstance());
DCHECK(partition);
context_ = static_cast<ServiceWorkerContextWrapper*>(
partition->GetServiceWorkerContext());
}
void ServiceWorkerHandler::SetClient(std::unique_ptr<Client> client) {
client_.swap(client);
}
void ServiceWorkerHandler::UpdateHosts() {
if (!enabled_)
return;
urls_.clear();
BrowserContext* browser_context = nullptr;
if (render_frame_host_) {
for (FrameTreeNode* node :
render_frame_host_->frame_tree_node()->frame_tree()->Nodes())
urls_.insert(node->current_url());
browser_context = render_frame_host_->GetProcess()->GetBrowserContext();
}
ServiceWorkerDevToolsAgentHost::Map old_hosts = attached_hosts_;
ServiceWorkerDevToolsAgentHost::Map new_hosts =
GetMatchingServiceWorkers(browser_context, urls_);
for (const auto& pair : old_hosts) {
if (new_hosts.find(pair.first) == new_hosts.end())
ReportWorkerTerminated(pair.second.get());
}
for (const 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);
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);
ClearForceUpdate();
for (const auto& pair : attached_hosts_)
pair.second->DetachClient(this);
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::SkipWaiting(const std::string& scope_url) {
if (!enabled_)
return Response::OK();
if (!context_)
return CreateContextErrorResponse();
context_->SkipWaitingWorker(GURL(scope_url));
return Response::OK();
}
Response ServiceWorkerHandler::StopWorker(const std::string& version_id) {
if (!enabled_)
return Response::OK();
if (!context_)
return CreateContextErrorResponse();
int64_t 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_t id = kInvalidServiceWorkerVersionId;
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::SetForceUpdateOnPageLoad(
bool force_update_on_page_load) {
if (!context_)
return CreateContextErrorResponse();
context_->SetForceUpdateOnPageLoad(force_update_on_page_load);
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_t id = 0;
if (!base::StringToInt64(registration_id, &id))
return CreateInvalidVersionIdErrorResponse();
PushEventPayload payload;
if (data.size() > 0)
payload.setData(data);
BrowserContext::DeliverPushMessage(
render_frame_host_->GetProcess()->GetBrowserContext(), GURL(origin), id,
payload, base::Bind(&PushDeliveryNoOp));
return Response::OK();
}
Response ServiceWorkerHandler::DispatchSyncEvent(
const std::string& origin,
const std::string& registration_id,
const std::string& tag,
bool last_chance) {
if (!enabled_)
return Response::OK();
if (!render_frame_host_)
return CreateContextErrorResponse();
int64_t id = 0;
if (!base::StringToInt64(registration_id, &id))
return CreateInvalidVersionIdErrorResponse();
StoragePartitionImpl* partition =
static_cast<StoragePartitionImpl*>(BrowserContext::GetStoragePartition(
render_frame_host_->GetProcess()->GetBrowserContext(),
render_frame_host_->GetSiteInstance()));
BackgroundSyncContext* sync_context = partition->GetBackgroundSyncContext();
BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
base::Bind(&DispatchSyncEventOnIO, context_,
make_scoped_refptr(sync_context),
GURL(origin), id, tag, last_chance));
return Response::OK();
}
Response ServiceWorkerHandler::GetTargetInfo(
const std::string& target_id,
scoped_refptr<TargetInfo>* target_info) {
scoped_refptr<DevToolsAgentHost> 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<DevToolsAgentHost> 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<DevToolsAgentHostImpl> 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<ServiceWorkerRegistrationInfo>& registrations) {
std::vector<scoped_refptr<ServiceWorkerRegistration>> 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<ServiceWorkerVersionInfo>& versions) {
std::vector<scoped_refptr<ServiceWorkerVersion>> 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_t registration_id,
int64_t 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::WorkerVersionInstalled(
ServiceWorkerDevToolsAgentHost* host) {
UpdateHosts();
}
void ServiceWorkerHandler::WorkerVersionDoomed(
ServiceWorkerDevToolsAgentHost* host) {
UpdateHosts();
}
void ServiceWorkerHandler::WorkerDestroyed(
ServiceWorkerDevToolsAgentHost* host) {
UpdateHosts();
}
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())
->set_version_id(base::Int64ToString(
host->service_worker_version_id())));
}
void ServiceWorkerHandler::ReportWorkerTerminated(
ServiceWorkerDevToolsAgentHost* host) {
auto it = attached_hosts_.find(host->GetId());
if (it == attached_hosts_.end())
return;
host->DetachClient(this);
client_->WorkerTerminated(WorkerTerminatedParams::Create()->
set_worker_id(host->GetId()));
attached_hosts_.erase(it);
}
void ServiceWorkerHandler::ClearForceUpdate() {
if (context_)
context_->SetForceUpdateOnPageLoad(false);
}
} // namespace service_worker
} // namespace devtools
} // namespace content