blob: 098b6b31df8e27efb326a1cc90137ad2657c8a5f [file] [log] [blame]
// Copyright 2013 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/service_worker/service_worker_register_job.h"
#include <vector>
#include "base/message_loop/message_loop.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_job_coordinator.h"
#include "content/browser/service_worker/service_worker_registration.h"
#include "content/browser/service_worker/service_worker_storage.h"
#include "content/browser/service_worker/service_worker_utils.h"
namespace content {
namespace {
void RunSoon(const base::Closure& closure) {
base::MessageLoop::current()->PostTask(FROM_HERE, closure);
}
}
typedef ServiceWorkerRegisterJobBase::RegistrationJobType RegistrationJobType;
ServiceWorkerRegisterJob::ServiceWorkerRegisterJob(
base::WeakPtr<ServiceWorkerContextCore> context,
const GURL& pattern,
const GURL& script_url)
: context_(context),
pattern_(pattern),
script_url_(script_url),
phase_(INITIAL),
is_promise_resolved_(false),
promise_resolved_status_(SERVICE_WORKER_OK),
weak_factory_(this) {}
ServiceWorkerRegisterJob::~ServiceWorkerRegisterJob() {
DCHECK(!context_ ||
phase_ == INITIAL || phase_ == COMPLETE || phase_ == ABORT)
<< "Jobs should only be interrupted during shutdown.";
}
void ServiceWorkerRegisterJob::AddCallback(const RegistrationCallback& callback,
int process_id) {
if (!is_promise_resolved_) {
callbacks_.push_back(callback);
if (process_id != -1 && (phase_ < UPDATE || !pending_version()))
pending_process_ids_.push_back(process_id);
return;
}
RunSoon(base::Bind(
callback, promise_resolved_status_,
promise_resolved_registration_, promise_resolved_version_));
}
void ServiceWorkerRegisterJob::Start() {
SetPhase(START);
context_->storage()->FindRegistrationForPattern(
pattern_,
base::Bind(
&ServiceWorkerRegisterJob::HandleExistingRegistrationAndContinue,
weak_factory_.GetWeakPtr()));
}
void ServiceWorkerRegisterJob::Abort() {
SetPhase(ABORT);
CompleteInternal(SERVICE_WORKER_ERROR_ABORT);
// Don't have to call FinishJob() because the caller takes care of removing
// the jobs from the queue.
}
bool ServiceWorkerRegisterJob::Equals(ServiceWorkerRegisterJobBase* job) {
if (job->GetType() != GetType())
return false;
ServiceWorkerRegisterJob* register_job =
static_cast<ServiceWorkerRegisterJob*>(job);
return register_job->pattern_ == pattern_ &&
register_job->script_url_ == script_url_;
}
RegistrationJobType ServiceWorkerRegisterJob::GetType() {
return REGISTRATION;
}
ServiceWorkerRegisterJob::Internal::Internal() {}
ServiceWorkerRegisterJob::Internal::~Internal() {}
void ServiceWorkerRegisterJob::set_registration(
ServiceWorkerRegistration* registration) {
DCHECK(phase_ == START || phase_ == REGISTER) << phase_;
DCHECK(!internal_.registration);
internal_.registration = registration;
}
ServiceWorkerRegistration* ServiceWorkerRegisterJob::registration() {
DCHECK(phase_ >= REGISTER) << phase_;
return internal_.registration;
}
void ServiceWorkerRegisterJob::set_pending_version(
ServiceWorkerVersion* version) {
DCHECK(phase_ == UPDATE) << phase_;
DCHECK(!internal_.pending_version);
internal_.pending_version = version;
}
ServiceWorkerVersion* ServiceWorkerRegisterJob::pending_version() {
DCHECK(phase_ >= UPDATE) << phase_;
return internal_.pending_version;
}
void ServiceWorkerRegisterJob::SetPhase(Phase phase) {
switch (phase) {
case INITIAL:
NOTREACHED();
break;
case START:
DCHECK(phase_ == INITIAL) << phase_;
break;
case REGISTER:
DCHECK(phase_ == START) << phase_;
break;
case UPDATE:
DCHECK(phase_ == START || phase_ == REGISTER) << phase_;
break;
case INSTALL:
DCHECK(phase_ == UPDATE) << phase_;
break;
case STORE:
DCHECK(phase_ == INSTALL) << phase_;
break;
case ACTIVATE:
DCHECK(phase_ == STORE) << phase_;
break;
case COMPLETE:
DCHECK(phase_ != INITIAL && phase_ != COMPLETE) << phase_;
break;
case ABORT:
break;
}
phase_ = phase;
}
// This function corresponds to the steps in Register following
// "Let serviceWorkerRegistration be _GetRegistration(scope)"
// |existing_registration| corresponds to serviceWorkerRegistration.
// Throughout this file, comments in quotes are excerpts from the spec.
void ServiceWorkerRegisterJob::HandleExistingRegistrationAndContinue(
ServiceWorkerStatusCode status,
const scoped_refptr<ServiceWorkerRegistration>& existing_registration) {
// On unexpected error, abort this registration job.
if (status != SERVICE_WORKER_ERROR_NOT_FOUND && status != SERVICE_WORKER_OK) {
Complete(status);
return;
}
// "If serviceWorkerRegistration is not null and script is equal to
// serviceWorkerRegistration.scriptUrl..." resolve with the existing
// registration and abort.
if (existing_registration.get() &&
existing_registration->script_url() == script_url_) {
set_registration(existing_registration);
// If there's no active version, go ahead to Update (this isn't in the spec
// but seems reasonable, and without SoftUpdate implemented we can never
// Update otherwise).
if (!existing_registration->active_version()) {
UpdateAndContinue(status);
return;
}
ResolvePromise(
status, existing_registration, existing_registration->active_version());
Complete(SERVICE_WORKER_OK);
return;
}
// "If serviceWorkerRegistration is null..." create a new registration.
if (!existing_registration.get()) {
RegisterAndContinue(SERVICE_WORKER_OK);
return;
}
// On script URL mismatch, "set serviceWorkerRegistration.scriptUrl to
// script." We accomplish this by deleting the existing registration and
// registering a new one.
// TODO(falken): Match the spec. We now throw away the active_version_ and
// waiting_version_ of the existing registration, which isn't in the spec.
// TODO(michaeln): Deactivate the live existing_registration object and
// eventually call storage->DeleteVersionResources()
// when it no longer has any controllees.
context_->storage()->DeleteRegistration(
existing_registration->id(),
existing_registration->script_url().GetOrigin(),
base::Bind(&ServiceWorkerRegisterJob::RegisterAndContinue,
weak_factory_.GetWeakPtr()));
}
// Creates a new ServiceWorkerRegistration.
void ServiceWorkerRegisterJob::RegisterAndContinue(
ServiceWorkerStatusCode status) {
SetPhase(REGISTER);
if (status != SERVICE_WORKER_OK) {
// Abort this registration job.
Complete(status);
return;
}
set_registration(new ServiceWorkerRegistration(
pattern_, script_url_, context_->storage()->NewRegistrationId(),
context_));
context_->storage()->NotifyInstallingRegistration(registration());
UpdateAndContinue(SERVICE_WORKER_OK);
}
// This function corresponds to the spec's _Update algorithm.
void ServiceWorkerRegisterJob::UpdateAndContinue(
ServiceWorkerStatusCode status) {
SetPhase(UPDATE);
if (status != SERVICE_WORKER_OK) {
// Abort this registration job.
Complete(status);
return;
}
// TODO(falken): "If serviceWorkerRegistration.installingWorker is not null.."
// then terminate the installing worker. It doesn't make sense to implement
// yet since we always activate the worker if install completed, so there can
// be no installing worker at this point.
// TODO(nhiroki): Check 'installing_version()' instead when it's supported.
DCHECK(!registration()->waiting_version());
// "Let serviceWorker be a newly-created ServiceWorker object..." and start
// the worker.
set_pending_version(new ServiceWorkerVersion(
registration(), context_->storage()->NewVersionId(), context_));
// TODO(michaeln): Start the worker into a paused state where the
// script resource is downloaded but not yet evaluated.
pending_version()->StartWorkerWithCandidateProcesses(
pending_process_ids_,
base::Bind(&ServiceWorkerRegisterJob::OnStartWorkerFinished,
weak_factory_.GetWeakPtr()));
}
void ServiceWorkerRegisterJob::OnStartWorkerFinished(
ServiceWorkerStatusCode status) {
// "If serviceWorker fails to start up..." then reject the promise with an
// error and abort.
if (status != SERVICE_WORKER_OK) {
Complete(status);
return;
}
// TODO(michaeln): Compare the old and new script.
// If different unpause the worker and continue with
// the job. If the same ResolvePromise with the current
// version and complete the job, throwing away the new version
// since there's nothing new.
// "Resolve promise with serviceWorker."
// Although the spec doesn't set waitingWorker until after resolving the
// promise, our system's resolving works by passing ServiceWorkerRegistration
// to the callbacks, so waitingWorker must be set first.
DCHECK(!registration()->waiting_version());
registration()->set_waiting_version(pending_version());
ResolvePromise(status, registration(), pending_version());
AssociateWaitingVersionToDocuments(context_, pending_version());
InstallAndContinue();
}
// This function corresponds to the spec's _Install algorithm.
void ServiceWorkerRegisterJob::InstallAndContinue() {
SetPhase(INSTALL);
// "Set serviceWorkerRegistration.installingWorker._state to installing."
// "Fire install event on the associated ServiceWorkerGlobalScope object."
pending_version()->DispatchInstallEvent(
-1,
base::Bind(&ServiceWorkerRegisterJob::OnInstallFinished,
weak_factory_.GetWeakPtr()));
}
void ServiceWorkerRegisterJob::OnInstallFinished(
ServiceWorkerStatusCode status) {
// "If any handler called waitUntil()..." and the resulting promise
// is rejected, abort.
// TODO(kinuko,falken): For some error cases (e.g. ServiceWorker is
// unexpectedly terminated) we may want to retry sending the event again.
if (status != SERVICE_WORKER_OK) {
Complete(status);
return;
}
SetPhase(STORE);
context_->storage()->StoreRegistration(
registration(),
pending_version(),
base::Bind(&ServiceWorkerRegisterJob::OnStoreRegistrationComplete,
weak_factory_.GetWeakPtr()));
}
void ServiceWorkerRegisterJob::OnStoreRegistrationComplete(
ServiceWorkerStatusCode status) {
if (status != SERVICE_WORKER_OK) {
Complete(status);
return;
}
ActivateAndContinue();
}
// This function corresponds to the spec's _Activate algorithm.
void ServiceWorkerRegisterJob::ActivateAndContinue() {
SetPhase(ACTIVATE);
// "If existingWorker is not null, then: wait for exitingWorker to finish
// handling any in-progress requests."
// See if we already have an active_version for the scope and it has
// controllee documents (if so activating the new version should wait
// until we have no documents controlled by the version).
if (registration()->active_version() &&
registration()->active_version()->HasControllee()) {
// TODO(kinuko,falken): Currently we immediately return if the existing
// registration already has an active version, so we shouldn't come
// this way.
NOTREACHED();
// TODO(falken): Register an continuation task to wait for NoControllees
// notification so that we can resume activation later (see comments
// in ServiceWorkerVersion::RemoveControllee).
Complete(SERVICE_WORKER_OK);
return;
}
// "Set serviceWorkerRegistration.waitingWorker to null."
// "Set serviceWorkerRegistration.activeWorker to activatingWorker."
DisassociateWaitingVersionFromDocuments(
context_, pending_version()->version_id());
registration()->set_waiting_version(NULL);
DCHECK(!registration()->active_version());
registration()->set_active_version(pending_version());
// "Set serviceWorkerRegistration.activeWorker._state to activating."
// "Fire activate event on the associated ServiceWorkerGlobalScope object."
// "Set serviceWorkerRegistration.activeWorker._state to active."
pending_version()->DispatchActivateEvent(
base::Bind(&ServiceWorkerRegisterJob::OnActivateFinished,
weak_factory_.GetWeakPtr()));
}
void ServiceWorkerRegisterJob::OnActivateFinished(
ServiceWorkerStatusCode status) {
// "If any handler called waitUntil()..." and the resulting promise
// is rejected, abort.
// TODO(kinuko,falken): For some error cases (e.g. ServiceWorker is
// unexpectedly terminated) we may want to retry sending the event again.
if (status != SERVICE_WORKER_OK) {
registration()->set_active_version(NULL);
Complete(status);
return;
}
context_->storage()->UpdateToActiveState(
registration(),
base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
Complete(SERVICE_WORKER_OK);
}
void ServiceWorkerRegisterJob::Complete(ServiceWorkerStatusCode status) {
CompleteInternal(status);
context_->job_coordinator()->FinishJob(pattern_, this);
}
void ServiceWorkerRegisterJob::CompleteInternal(
ServiceWorkerStatusCode status) {
SetPhase(COMPLETE);
if (status != SERVICE_WORKER_OK) {
if (registration() && registration()->waiting_version()) {
DisassociateWaitingVersionFromDocuments(
context_, registration()->waiting_version()->version_id());
registration()->set_waiting_version(NULL);
}
if (registration() && !registration()->active_version()) {
context_->storage()->DeleteRegistration(
registration()->id(),
registration()->script_url().GetOrigin(),
base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
}
if (!is_promise_resolved_)
ResolvePromise(status, NULL, NULL);
}
DCHECK(callbacks_.empty());
if (registration()) {
context_->storage()->NotifyDoneInstallingRegistration(
registration(), pending_version(), status);
}
}
void ServiceWorkerRegisterJob::ResolvePromise(
ServiceWorkerStatusCode status,
ServiceWorkerRegistration* registration,
ServiceWorkerVersion* version) {
DCHECK(!is_promise_resolved_);
is_promise_resolved_ = true;
promise_resolved_status_ = status;
promise_resolved_registration_ = registration;
promise_resolved_version_ = version;
for (std::vector<RegistrationCallback>::iterator it = callbacks_.begin();
it != callbacks_.end();
++it) {
it->Run(status, registration, version);
}
callbacks_.clear();
}
// static
void ServiceWorkerRegisterJob::AssociateWaitingVersionToDocuments(
base::WeakPtr<ServiceWorkerContextCore> context,
ServiceWorkerVersion* version) {
DCHECK(context);
DCHECK(version);
for (scoped_ptr<ServiceWorkerContextCore::ProviderHostIterator> it =
context->GetProviderHostIterator();
!it->IsAtEnd();
it->Advance()) {
ServiceWorkerProviderHost* host = it->GetProviderHost();
if (!host->IsContextAlive())
continue;
if (ServiceWorkerUtils::ScopeMatches(version->scope(),
host->document_url())) {
// The spec's _Update algorithm says, "upgrades active version to a new
// version for the same URL scope.", so skip if the scope (registration)
// of |version| is different from that of the current active/waiting
// version.
if (!host->ValidateVersionForAssociation(version))
continue;
// TODO(nhiroki): Keep |host->waiting_version()| to be replaced and set
// status of them to 'redandunt' after breaking the loop.
host->SetWaitingVersion(version);
// TODO(nhiroki): Set |host|'s installing version to null.
}
}
}
// static
void ServiceWorkerRegisterJob::DisassociateWaitingVersionFromDocuments(
base::WeakPtr<ServiceWorkerContextCore> context,
int64 version_id) {
DCHECK(context);
for (scoped_ptr<ServiceWorkerContextCore::ProviderHostIterator> it =
context->GetProviderHostIterator();
!it->IsAtEnd();
it->Advance()) {
ServiceWorkerProviderHost* host = it->GetProviderHost();
if (!host->IsContextAlive())
continue;
if (host->waiting_version() &&
host->waiting_version()->version_id() == version_id) {
host->SetWaitingVersion(NULL);
}
}
}
} // namespace content