From 81786cc7e658cf628fa296518286a2b1eeefb018 Mon Sep 17 00:00:00 2001 From: Nikhil Marathe Date: Wed, 11 Jun 2014 09:12:56 -0700 Subject: [PATCH] Bug 984048: Part 3 - Update algorithm. r=ehsan,khuey --HG-- extra : rebase_source : 5e80b118faeea419cfebbc2d1060cd2a1a61bcba --- dom/workers/ServiceWorkerManager.cpp | 359 +++++++++++++++++- dom/workers/ServiceWorkerManager.h | 97 ++++- dom/workers/test/serviceworkers/mochitest.ini | 6 + .../test_installation_simple.html | 48 ++- dom/workers/test/serviceworkers/worker.js | 1 + dom/workers/test/serviceworkers/worker2.js | 1 + dom/workers/test/serviceworkers/worker3.js | 1 + 7 files changed, 475 insertions(+), 38 deletions(-) create mode 100644 dom/workers/test/serviceworkers/worker.js create mode 100644 dom/workers/test/serviceworkers/worker2.js create mode 100644 dom/workers/test/serviceworkers/worker3.js diff --git a/dom/workers/ServiceWorkerManager.cpp b/dom/workers/ServiceWorkerManager.cpp index 36c74a812ff..ee0ae487ecb 100644 --- a/dom/workers/ServiceWorkerManager.cpp +++ b/dom/workers/ServiceWorkerManager.cpp @@ -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> array; + array.SwapElements(mPromises); + for (uint32_t i = 0; i < array.Length(); ++i) { + nsTWeakRef& pendingPromise = array.ElementAt(i); + if (pendingPromise) { + nsCOMPtr go = + do_QueryInterface(pendingPromise->GetParentObject()); + MOZ_ASSERT(go); + + AutoSafeJSContext cx; + JS::Rooted global(cx, go->GetGlobalJSObject()); + JSAutoCompartment ac(cx, global); + + GlobalObject domGlobal(cx, global); + + nsRefPtr 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> array; + array.SwapElements(mPromises); + for (uint32_t i = 0; i < array.Length(); ++i) { + nsTWeakRef& pendingPromise = array.ElementAt(i); + if (pendingPromise) { + // Since ServiceWorkerContainer is only exposed to windows we can be + // certain about this cast. + nsCOMPtr window = + do_QueryInterface(pendingPromise->GetParentObject()); + MOZ_ASSERT(window); + nsRefPtr domError = new DOMError(window, aRv); + pendingPromise->MaybeRejectBrokenly(domError); + } + } +} + +class FinishFetchOnMainThreadRunnable : public nsRunnable +{ + nsMainThreadPtrHandle mUpdateInstance; +public: + FinishFetchOnMainThreadRunnable + (const nsMainThreadPtrHandle& aUpdateInstance) + : mUpdateInstance(aUpdateInstance) + { } + + NS_IMETHOD + Run() MOZ_OVERRIDE; +}; + +class FinishSuccessfulFetchWorkerRunnable : public WorkerRunnable +{ + nsMainThreadPtrHandle mUpdateInstance; +public: + FinishSuccessfulFetchWorkerRunnable(WorkerPrivate* aWorkerPrivate, + const nsMainThreadPtrHandle& aUpdateInstance) + : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount), + mUpdateInstance(aUpdateInstance) + { + AssertIsOnMainThread(); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + aWorkerPrivate->AssertIsOnWorkerThread(); + if (!aWorkerPrivate->WorkerScriptExecutedSuccessfully()) { + return true; + } + + nsRefPtr 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 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 swm = ServiceWorkerManager::GetInstance(); + MOZ_ASSERT(swm); + + nsRefPtr 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 handle = + new nsMainThreadPtrHolder(this); + // FIXME(nsm): Deal with error case (worker failed to download, redirect, + // parse) in error handler patch. + nsRefPtr 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 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 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 registration = domainInfo->GetRegistration(mScope); + nsRefPtr 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::GetInstance() { - nsCOMPtr swm = do_GetService(SERVICEWORKERMANAGER_CONTRACTID); + nsCOMPtr swm = + do_GetService(SERVICEWORKERMANAGER_CONTRACTID); nsRefPtr 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 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, diff --git a/dom/workers/ServiceWorkerManager.h b/dom/workers/ServiceWorkerManager.h index c6191c5afbb..293a5ae7d77 100644 --- a/dom/workers/ServiceWorkerManager.h +++ b/dom/workers/ServiceWorkerManager.h @@ -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> 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 mUpdatePromise; + nsRefPtr 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 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 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, diff --git a/dom/workers/test/serviceworkers/mochitest.ini b/dom/workers/test/serviceworkers/mochitest.ini index f7b5b7d5690..1ba39eb1fb1 100644 --- a/dom/workers/test/serviceworkers/mochitest.ini +++ b/dom/workers/test/serviceworkers/mochitest.ini @@ -1,2 +1,8 @@ +[DEFAULT] +support-files = + worker.js + worker2.js + worker3.js + [test_installation_simple.html] [test_navigator.html] diff --git a/dom/workers/test/serviceworkers/test_installation_simple.html b/dom/workers/test/serviceworkers/test_installation_simple.html index 97a0a9394d7..cf84bd80abe 100644 --- a/dom/workers/test/serviceworkers/test_installation_simple.html +++ b/dom/workers/test/serviceworkers/test_installation_simple.html @@ -16,7 +16,7 @@