Bug 984048: Part 3 - Update algorithm. r=ehsan,khuey

--HG--
extra : rebase_source : 5e80b118faeea419cfebbc2d1060cd2a1a61bcba
This commit is contained in:
Nikhil Marathe 2014-06-11 09:12:56 -07:00
parent a90f337925
commit 81786cc7e6
7 changed files with 475 additions and 38 deletions

View File

@ -11,20 +11,242 @@
#include "mozilla/Preferences.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/DOMError.h"
#include "nsContentUtils.h"
#include "nsCxPusher.h"
#include "nsNetUtil.h"
#include "nsProxyRelease.h"
#include "nsTArray.h"
#include "RuntimeService.h"
#include "ServiceWorker.h"
#include "WorkerInlines.h"
#include "WorkerPrivate.h"
#include "WorkerRunnable.h"
using namespace mozilla;
using namespace mozilla::dom;
BEGIN_WORKERS_NAMESPACE
NS_IMPL_ISUPPORTS0(ServiceWorkerRegistration)
UpdatePromise::UpdatePromise()
: mState(Pending)
{
MOZ_COUNT_CTOR(UpdatePromise);
}
UpdatePromise::~UpdatePromise()
{
MOZ_COUNT_DTOR(UpdatePromise);
}
void
UpdatePromise::AddPromise(Promise* aPromise)
{
MOZ_ASSERT(mState == Pending);
mPromises.AppendElement(aPromise);
}
void
UpdatePromise::ResolveAllPromises(const nsACString& aScriptSpec, const nsACString& aScope)
{
AssertIsOnMainThread();
MOZ_ASSERT(mState == Pending);
mState = Resolved;
RuntimeService* rs = RuntimeService::GetOrCreateService();
MOZ_ASSERT(rs);
nsTArray<nsTWeakRef<Promise>> array;
array.SwapElements(mPromises);
for (uint32_t i = 0; i < array.Length(); ++i) {
nsTWeakRef<Promise>& pendingPromise = array.ElementAt(i);
if (pendingPromise) {
nsCOMPtr<nsIGlobalObject> go =
do_QueryInterface(pendingPromise->GetParentObject());
MOZ_ASSERT(go);
AutoSafeJSContext cx;
JS::Rooted<JSObject*> global(cx, go->GetGlobalJSObject());
JSAutoCompartment ac(cx, global);
GlobalObject domGlobal(cx, global);
nsRefPtr<ServiceWorker> serviceWorker;
nsresult rv = rs->CreateServiceWorker(domGlobal,
NS_ConvertUTF8toUTF16(aScriptSpec),
aScope,
getter_AddRefs(serviceWorker));
if (NS_WARN_IF(NS_FAILED(rv))) {
pendingPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
continue;
}
pendingPromise->MaybeResolve(serviceWorker);
}
}
}
void
UpdatePromise::RejectAllPromises(nsresult aRv)
{
AssertIsOnMainThread();
MOZ_ASSERT(mState == Pending);
mState = Rejected;
nsTArray<nsTWeakRef<Promise>> array;
array.SwapElements(mPromises);
for (uint32_t i = 0; i < array.Length(); ++i) {
nsTWeakRef<Promise>& pendingPromise = array.ElementAt(i);
if (pendingPromise) {
// Since ServiceWorkerContainer is only exposed to windows we can be
// certain about this cast.
nsCOMPtr<nsPIDOMWindow> window =
do_QueryInterface(pendingPromise->GetParentObject());
MOZ_ASSERT(window);
nsRefPtr<DOMError> domError = new DOMError(window, aRv);
pendingPromise->MaybeRejectBrokenly(domError);
}
}
}
class FinishFetchOnMainThreadRunnable : public nsRunnable
{
nsMainThreadPtrHandle<ServiceWorkerUpdateInstance> mUpdateInstance;
public:
FinishFetchOnMainThreadRunnable
(const nsMainThreadPtrHandle<ServiceWorkerUpdateInstance>& aUpdateInstance)
: mUpdateInstance(aUpdateInstance)
{ }
NS_IMETHOD
Run() MOZ_OVERRIDE;
};
class FinishSuccessfulFetchWorkerRunnable : public WorkerRunnable
{
nsMainThreadPtrHandle<ServiceWorkerUpdateInstance> mUpdateInstance;
public:
FinishSuccessfulFetchWorkerRunnable(WorkerPrivate* aWorkerPrivate,
const nsMainThreadPtrHandle<ServiceWorkerUpdateInstance>& aUpdateInstance)
: WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount),
mUpdateInstance(aUpdateInstance)
{
AssertIsOnMainThread();
}
bool
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
{
aWorkerPrivate->AssertIsOnWorkerThread();
if (!aWorkerPrivate->WorkerScriptExecutedSuccessfully()) {
return true;
}
nsRefPtr<FinishFetchOnMainThreadRunnable> r =
new FinishFetchOnMainThreadRunnable(mUpdateInstance);
NS_DispatchToMainThread(r);
return true;
}
};
// Allows newer calls to Update() to 'abort' older calls.
// Each call to Update() creates the instance which handles creating the
// worker and queues up a runnable to resolve the update promise once the
// worker has successfully been parsed.
class ServiceWorkerUpdateInstance MOZ_FINAL : public nsISupports
{
// Owner of this instance.
ServiceWorkerRegistration* mRegistration;
nsCString mScriptSpec;
nsCOMPtr<nsPIDOMWindow> mWindow;
bool mAborted;
public:
NS_DECL_ISUPPORTS
ServiceWorkerUpdateInstance(ServiceWorkerRegistration *aRegistration,
nsPIDOMWindow* aWindow)
: mRegistration(aRegistration),
// Capture the current script spec in case register() gets called.
mScriptSpec(aRegistration->mScriptSpec),
mWindow(aWindow),
mAborted(false)
{
AssertIsOnMainThread();
}
void
Abort()
{
MOZ_ASSERT(!mAborted);
mAborted = true;
}
void
Update()
{
AssertIsOnMainThread();
nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
MOZ_ASSERT(swm);
nsRefPtr<ServiceWorker> serviceWorker;
nsresult rv = swm->CreateServiceWorkerForWindow(mWindow,
mScriptSpec,
mRegistration->mScope,
getter_AddRefs(serviceWorker));
if (NS_WARN_IF(NS_FAILED(rv))) {
swm->RejectUpdatePromiseObservers(mRegistration, rv);
return;
}
nsMainThreadPtrHandle<ServiceWorkerUpdateInstance> handle =
new nsMainThreadPtrHolder<ServiceWorkerUpdateInstance>(this);
// FIXME(nsm): Deal with error case (worker failed to download, redirect,
// parse) in error handler patch.
nsRefPtr<FinishSuccessfulFetchWorkerRunnable> r =
new FinishSuccessfulFetchWorkerRunnable(serviceWorker->GetWorkerPrivate(), handle);
AutoSafeJSContext cx;
if (!r->Dispatch(cx)) {
swm->RejectUpdatePromiseObservers(mRegistration, NS_ERROR_FAILURE);
}
}
void
FetchDone()
{
AssertIsOnMainThread();
if (mAborted) {
return;
}
nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
MOZ_ASSERT(swm);
swm->FinishFetch(mRegistration, mWindow);
}
};
NS_IMPL_ISUPPORTS0(ServiceWorkerUpdateInstance)
NS_IMETHODIMP
FinishFetchOnMainThreadRunnable::Run()
{
AssertIsOnMainThread();
mUpdateInstance->FetchDone();
return NS_OK;
}
ServiceWorkerRegistration::ServiceWorkerRegistration(const nsACString& aScope)
: mScope(aScope),
mPendingUninstall(false)
{ }
ServiceWorkerRegistration::~ServiceWorkerRegistration()
{ }
//////////////////////////
// ServiceWorkerManager //
//////////////////////////
@ -89,13 +311,15 @@ public:
nsRefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
ServiceWorkerManager::ServiceWorkerDomainInfo* domainInfo =
swm->mDomainMap.Get(domain);
// FIXME(nsm): Refactor this pattern.
// XXXnsm: This pattern can be refactored if we end up using it
// often enough.
if (!swm->mDomainMap.Get(domain, &domainInfo)) {
domainInfo = new ServiceWorkerManager::ServiceWorkerDomainInfo;
swm->mDomainMap.Put(domain, domainInfo);
}
nsRefPtr<ServiceWorkerRegistration> registration = domainInfo->GetRegistration(mScope);
nsRefPtr<ServiceWorkerRegistration> registration =
domainInfo->GetRegistration(mScope);
nsCString spec;
rv = mScriptURI->GetSpec(spec);
@ -107,9 +331,6 @@ public:
if (registration) {
registration->mPendingUninstall = false;
if (spec.Equals(registration->mScriptSpec)) {
// FIXME(nsm): Force update on Shift+Reload. Algorithm not specified for
// that yet.
// There is an existing update in progress. Resolve with whatever it
// results in.
if (registration->HasUpdatePromise()) {
@ -117,8 +338,8 @@ public:
return NS_OK;
}
// There is no update in progress and since SW updating is upto the UA, we
// will not update right now. Simply resolve with whatever worker we
// There is no update in progress and since SW updating is upto the UA,
// we will not update right now. Simply resolve with whatever worker we
// have.
ServiceWorkerInfo info = registration->Newest();
if (info.IsValid()) {
@ -143,10 +364,16 @@ public:
registration->mScriptSpec = spec;
// FIXME(nsm): Call Update. Same bug, different patch.
// For now if the registration reaches this spot, the promise remains
// unresolved.
return NS_OK;
rv = swm->Update(registration, mWindow);
MOZ_ASSERT(registration->HasUpdatePromise());
// We append this register() call's promise after calling Update() because
// we don't want this one to be aborted when the others (existing updates
// for the same registration) are aborted. Update() sets a new
// UpdatePromise on the registration.
registration->mUpdatePromise->AddPromise(mPromise);
return rv;
}
};
@ -154,7 +381,8 @@ public:
// automatically reject the Promise.
NS_IMETHODIMP
ServiceWorkerManager::Register(nsIDOMWindow* aWindow, const nsAString& aScope,
const nsAString& aScriptURL, nsISupports** aPromise)
const nsAString& aScriptURL,
nsISupports** aPromise)
{
AssertIsOnMainThread();
MOZ_ASSERT(aWindow);
@ -206,10 +434,9 @@ ServiceWorkerManager::Register(nsIDOMWindow* aWindow, const nsAString& aScope,
return rv;
}
// https://github.com/slightlyoff/ServiceWorker/issues/262
// allowIfInheritsPrincipal: allow data: URLs for now.
// Data URLs are not allowed.
rv = documentPrincipal->CheckMayLoad(scriptURI, true /* report */,
true /* allowIfInheritsPrincipal */);
false /* allowIfInheritsPrincipal */);
if (NS_FAILED(rv)) {
return NS_ERROR_DOM_SECURITY_ERR;
}
@ -238,11 +465,51 @@ ServiceWorkerManager::Register(nsIDOMWindow* aWindow, const nsAString& aScope,
return NS_DispatchToCurrentThread(registerRunnable);
}
void
ServiceWorkerManager::RejectUpdatePromiseObservers(ServiceWorkerRegistration* aRegistration,
nsresult aRv)
{
AssertIsOnMainThread();
MOZ_ASSERT(aRegistration->HasUpdatePromise());
aRegistration->mUpdatePromise->RejectAllPromises(aRv);
aRegistration->mUpdatePromise = nullptr;
}
/*
* Update() does not return the Promise that the spec says it should. Callers
* may access the registration's (new) Promise after calling this method.
*/
NS_IMETHODIMP
ServiceWorkerManager::Update(ServiceWorkerRegistration* aRegistration,
nsPIDOMWindow* aWindow)
{
// FIXME(nsm): Same bug, different patch.
if (aRegistration->HasUpdatePromise()) {
NS_WARNING("Already had a UpdatePromise. Aborting that one!");
RejectUpdatePromiseObservers(aRegistration, NS_ERROR_DOM_ABORT_ERR);
MOZ_ASSERT(aRegistration->mUpdateInstance);
aRegistration->mUpdateInstance->Abort();
aRegistration->mUpdateInstance = nullptr;
}
if (aRegistration->mInstallingWorker.IsValid()) {
// FIXME(nsm): Terminate the worker. We still haven't figured out worker
// instance ownership when not associated with a window, so let's wait on
// this.
// FIXME(nsm): We should be setting the state on the actual worker
// instance.
// FIXME(nsm): Fire "statechange" on installing worker instance.
aRegistration->mInstallingWorker.Invalidate();
}
aRegistration->mUpdatePromise = new UpdatePromise();
// FIXME(nsm): Bug 931249. If we don't need to fetch & install, resolve
// promise and skip this.
// FIXME(nsm): Bug 931249. Force cache update if > 1 day.
aRegistration->mUpdateInstance =
new ServiceWorkerUpdateInstance(aRegistration, aWindow);
aRegistration->mUpdateInstance->Update();
return NS_OK;
}
@ -266,11 +533,69 @@ ServiceWorkerManager::Unregister(nsIDOMWindow* aWindow, const nsAString& aScope,
already_AddRefed<ServiceWorkerManager>
ServiceWorkerManager::GetInstance()
{
nsCOMPtr<nsIServiceWorkerManager> swm = do_GetService(SERVICEWORKERMANAGER_CONTRACTID);
nsCOMPtr<nsIServiceWorkerManager> swm =
do_GetService(SERVICEWORKERMANAGER_CONTRACTID);
nsRefPtr<ServiceWorkerManager> concrete = do_QueryObject(swm);
return concrete.forget();
}
void
ServiceWorkerManager::ResolveRegisterPromises(ServiceWorkerRegistration* aRegistration,
const nsACString& aWorkerScriptSpec)
{
AssertIsOnMainThread();
MOZ_ASSERT(aRegistration->HasUpdatePromise());
if (aRegistration->mUpdatePromise->IsRejected()) {
aRegistration->mUpdatePromise = nullptr;
return;
}
aRegistration->mUpdatePromise->ResolveAllPromises(aWorkerScriptSpec,
aRegistration->mScope);
aRegistration->mUpdatePromise = nullptr;
}
// Must NS_Free() aString
void
ServiceWorkerManager::FinishFetch(ServiceWorkerRegistration* aRegistration,
nsPIDOMWindow* aWindow)
{
AssertIsOnMainThread();
MOZ_ASSERT(aRegistration->HasUpdatePromise());
MOZ_ASSERT(aRegistration->mUpdateInstance);
aRegistration->mUpdateInstance = nullptr;
if (aRegistration->mUpdatePromise->IsRejected()) {
aRegistration->mUpdatePromise = nullptr;
return;
}
// We have skipped Steps 3-8.3 of the Update algorithm here!
nsRefPtr<ServiceWorker> worker;
nsresult rv = CreateServiceWorkerForWindow(aWindow,
aRegistration->mScriptSpec,
aRegistration->mScope,
getter_AddRefs(worker));
if (NS_WARN_IF(NS_FAILED(rv))) {
RejectUpdatePromiseObservers(aRegistration, rv);
return;
}
ResolveRegisterPromises(aRegistration, aRegistration->mScriptSpec);
ServiceWorkerInfo info(aRegistration->mScriptSpec);
Install(aRegistration, info);
}
void
ServiceWorkerManager::Install(ServiceWorkerRegistration* aRegistration,
ServiceWorkerInfo aServiceWorkerInfo)
{
// FIXME(nsm): Same bug, different patch.
}
NS_IMETHODIMP
ServiceWorkerManager::CreateServiceWorkerForWindow(nsPIDOMWindow* aWindow,
const nsACString& aScriptSpec,

View File

@ -23,6 +23,44 @@ namespace dom {
namespace workers {
class ServiceWorker;
class ServiceWorkerUpdateInstance;
/**
* UpdatePromise is a utility class that sort of imitates Promise, but not
* completely. Using DOM Promise from C++ is a pain when we know the precise types
* we're dealing with since it involves dealing with JSAPI. In this case we
* also don't (yet) need the 'thenables added after resolution should trigger
* immediately' support and other things like that. All we want is something
* that works reasonably Promise like and can resolve real DOM Promises added
* pre-emptively.
*/
class UpdatePromise MOZ_FINAL
{
public:
UpdatePromise();
~UpdatePromise();
void AddPromise(Promise* aPromise);
void ResolveAllPromises(const nsACString& aScriptSpec, const nsACString& aScope);
void RejectAllPromises(nsresult aRv);
bool
IsRejected() const
{
return mState == Rejected;
}
private:
enum {
Pending,
Resolved,
Rejected
} mState;
// XXXnsm: Right now we don't need to support AddPromise() after
// already being resolved (i.e. true Promise-like behaviour).
nsTArray<nsTWeakRef<Promise>> mPromises;
};
/*
* Wherever the spec treats a worker instance and a description of said worker
@ -32,7 +70,7 @@ class ServiceWorker;
*/
class ServiceWorkerInfo
{
const nsCString mScriptSpec;
nsCString mScriptSpec;
public:
bool
@ -41,6 +79,12 @@ public:
return !mScriptSpec.IsVoid();
}
void
Invalidate()
{
mScriptSpec.SetIsVoid(true);
}
const nsCString&
GetScriptSpec() const
{
@ -49,19 +93,23 @@ public:
}
ServiceWorkerInfo()
{ }
{
Invalidate();
}
explicit ServiceWorkerInfo(const nsACString& aScriptSpec)
: mScriptSpec(aScriptSpec)
{ }
};
class ServiceWorkerRegistration
// Needs to inherit from nsISupports because NS_ProxyRelease() does not support
// non-ISupports classes.
class ServiceWorkerRegistration MOZ_FINAL : public nsISupports
{
private:
~ServiceWorkerRegistration() {}
virtual ~ServiceWorkerRegistration();
public:
NS_INLINE_DECL_REFCOUNTING(ServiceWorkerRegistration)
NS_DECL_ISUPPORTS
nsCString mScope;
// The scriptURL for the registration. This may be completely different from
@ -72,18 +120,20 @@ public:
ServiceWorkerInfo mWaitingWorker;
ServiceWorkerInfo mInstallingWorker;
bool mHasUpdatePromise;
nsAutoPtr<UpdatePromise> mUpdatePromise;
nsRefPtr<ServiceWorkerUpdateInstance> mUpdateInstance;
void
AddUpdatePromiseObserver(Promise* aPromise)
{
// FIXME(nsm): Same bug, different patch.
MOZ_ASSERT(HasUpdatePromise());
mUpdatePromise->AddPromise(aPromise);
}
bool
HasUpdatePromise()
{
return mHasUpdatePromise;
return mUpdatePromise;
}
// When unregister() is called on a registration, it is not immediately
@ -91,11 +141,7 @@ public:
// pendingUninstall and when all controlling documents go away, removed.
bool mPendingUninstall;
explicit ServiceWorkerRegistration(const nsACString& aScope)
: mScope(aScope),
mHasUpdatePromise(false),
mPendingUninstall(false)
{ }
explicit ServiceWorkerRegistration(const nsACString& aScope);
ServiceWorkerInfo
Newest() const
@ -126,6 +172,8 @@ public:
class ServiceWorkerManager MOZ_FINAL : public nsIServiceWorkerManager
{
friend class RegisterRunnable;
friend class CallInstallRunnable;
friend class ServiceWorkerUpdateInstance;
public:
NS_DECL_ISUPPORTS
@ -174,12 +222,17 @@ public:
nsClassHashtable<nsCStringHashKey, ServiceWorkerDomainInfo> mDomainMap;
// FIXME(nsm): What do we do if a page calls for register("/foo_worker.js", { scope: "/*"
// }) and then another page calls register("/bar_worker.js", { scope: "/*" })
// while the install is in progress. The async install steps for register
// bar_worker.js could finish before foo_worker.js, but bar_worker still has
// to be the winning controller.
// FIXME(nsm): Move this into per domain?
void
ResolveRegisterPromises(ServiceWorkerRegistration* aRegistration,
const nsACString& aWorkerScriptSpec);
void
RejectUpdatePromiseObservers(ServiceWorkerRegistration* aRegistration,
nsresult aResult);
void
FinishFetch(ServiceWorkerRegistration* aRegistration,
nsPIDOMWindow* aWindow);
static already_AddRefed<ServiceWorkerManager>
GetInstance();
@ -191,6 +244,10 @@ private:
NS_IMETHOD
Update(ServiceWorkerRegistration* aRegistration, nsPIDOMWindow* aWindow);
void
Install(ServiceWorkerRegistration* aRegistration,
ServiceWorkerInfo aServiceWorkerInfo);
NS_IMETHODIMP
CreateServiceWorkerForWindow(nsPIDOMWindow* aWindow,
const nsACString& aScriptSpec,

View File

@ -1,2 +1,8 @@
[DEFAULT]
support-files =
worker.js
worker2.js
worker3.js
[test_installation_simple.html]
[test_navigator.html]

View File

@ -16,7 +16,7 @@
<script class="testbody" type="text/javascript">
function simpleRegister() {
var p = navigator.serviceWorker.register("/fake_worker.js");
var p = navigator.serviceWorker.register("worker.js");
ok(p instanceof Promise, "register() should return a Promise");
return Promise.resolve();
}
@ -50,14 +50,60 @@
ok(false, "non-HTTPS pages cannot register ServiceWorkers");
}, function(e) {
ok(e.name === "SecurityError", "Should fail with a SecurityError");
}).then(function() {
return new Promise((resolve) => SpecialPowers.popPrefEnv(resolve));
});
}
function realWorker() {
var p = navigator.serviceWorker.register("worker.js");
return p.then(function(w) {
ok(w instanceof ServiceWorker, "Register a ServiceWorker");
info(w.scope);
ok(w.scope == (new URL("/*", document.baseURI)).href, "Scope should match");
ok(w.url == (new URL("worker.js", document.baseURI)).href, "URL should be of the worker");
}, function(e) {
info(e.name);
ok(false, "Registration should have succeeded!");
});
}
function abortPrevious() {
var p = navigator.serviceWorker.register("worker2.js", { scope: "foo/*" });
var q = navigator.serviceWorker.register("worker3.js", { scope: "foo/*" });
return Promise.all([
p.then(function(w) {
ok(false, "First registration should fail with AbortError");
}, function(e) {
ok(e.name === "AbortError", "First registration should fail with AbortError");
}),
q.then(function(w) {
ok(w instanceof ServiceWorker, "Second registration should succeed");
}, function(e) {
ok(false, "Second registration should succeed");
})
]);
}
function networkError404() {
return navigator.serviceWorker.register("404.js").then(function(w) {
todo(false, "Should fail with NetworkError");
}, function(e) {
todo(e.name === "NetworkError", "Should fail with NetworkError");
});
}
function runTest() {
simpleRegister()
.then(sameOriginWorker)
.then(sameOriginScope)
.then(httpsOnly)
.then(realWorker)
.then(abortPrevious)
// FIXME(nsm): Uncomment once we have the error trapping patch from Bug 984048.
// .then(networkError404)
// put more tests here.
.then(function() {
SimpleTest.finish();

View File

@ -0,0 +1 @@
// empty worker, always succeed!

View File

@ -0,0 +1 @@
// worker2.js

View File

@ -0,0 +1 @@
// worker3.js