diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/workers | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/workers')
443 files changed, 47778 insertions, 0 deletions
diff --git a/dom/workers/ChromeWorker.cpp b/dom/workers/ChromeWorker.cpp new file mode 100644 index 0000000000..7fda84f7f5 --- /dev/null +++ b/dom/workers/ChromeWorker.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ChromeWorker.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/WorkerBinding.h" +#include "nsContentUtils.h" +#include "nsIXPConnect.h" +#include "WorkerPrivate.h" + +namespace mozilla::dom { + +/* static */ +already_AddRefed<ChromeWorker> ChromeWorker::Constructor( + const GlobalObject& aGlobal, const nsAString& aScriptURL, + const WorkerOptions& aOptions, ErrorResult& aRv) { + // Dump the JS stack if somebody's creating a ChromeWorker after shutdown has + // begun. See bug 1813353. + if (xpc::IsInAutomation() && + AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown)) { + NS_WARNING("ChromeWorker construction during shutdown"); + nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect(); + Unused << xpc->DebugDumpJSStack(true, true, false); + } + + JSContext* cx = aGlobal.Context(); + + RefPtr<WorkerPrivate> workerPrivate = WorkerPrivate::Constructor( + cx, aScriptURL, true /* aIsChromeWorker */, WorkerKindDedicated, + RequestCredentials::Omit, aOptions.mType, aOptions.mName, VoidCString(), + nullptr /*aLoadInfo */, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> globalObject = + do_QueryInterface(aGlobal.GetAsSupports()); + + RefPtr<ChromeWorker> worker = + new ChromeWorker(globalObject, workerPrivate.forget()); + return worker.forget(); +} + +/* static */ +bool ChromeWorker::WorkerAvailable(JSContext* aCx, JSObject* /* unused */) { + // Chrome is always allowed to use workers, and content is never + // allowed to use ChromeWorker, so all we have to check is the + // caller. However, chrome workers apparently might not have a + // system principal, so we have to check for them manually. + if (NS_IsMainThread()) { + return nsContentUtils::IsSystemCaller(aCx); + } + + return GetWorkerPrivateFromContext(aCx)->IsChromeWorker(); +} + +ChromeWorker::ChromeWorker(nsIGlobalObject* aGlobalObject, + already_AddRefed<WorkerPrivate> aWorkerPrivate) + : Worker(aGlobalObject, std::move(aWorkerPrivate)) {} + +ChromeWorker::~ChromeWorker() = default; + +JSObject* ChromeWorker::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + JS::Rooted<JSObject*> wrapper( + aCx, ChromeWorker_Binding::Wrap(aCx, this, aGivenProto)); + if (wrapper) { + // Most DOM objects don't assume they have a reflector. If they don't have + // one and need one, they create it. But in workers code, we assume that the + // reflector is always present. In order to guarantee that it's always + // present, we have to preserve it. Otherwise the GC will happily collect it + // as needed. + MOZ_ALWAYS_TRUE(TryPreserveWrapper(wrapper)); + } + + return wrapper; +} + +} // namespace mozilla::dom diff --git a/dom/workers/ChromeWorker.h b/dom/workers/ChromeWorker.h new file mode 100644 index 0000000000..04265f4a31 --- /dev/null +++ b/dom/workers/ChromeWorker.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ChromeWorker_h +#define mozilla_dom_ChromeWorker_h + +#include "mozilla/dom/Worker.h" + +namespace mozilla::dom { + +class ChromeWorker final : public Worker { + public: + static already_AddRefed<ChromeWorker> Constructor( + const GlobalObject& aGlobal, const nsAString& aScriptURL, + const WorkerOptions& aOptions, ErrorResult& aRv); + + static bool WorkerAvailable(JSContext* aCx, JSObject* /* unused */); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ChromeWorker(nsIGlobalObject* aGlobalObject, + already_AddRefed<WorkerPrivate> aWorkerPrivate); + ~ChromeWorker(); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ChromeWorker_h */ diff --git a/dom/workers/ChromeWorkerScope.cpp b/dom/workers/ChromeWorkerScope.cpp new file mode 100644 index 0000000000..e2ec334016 --- /dev/null +++ b/dom/workers/ChromeWorkerScope.cpp @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ChromeWorkerScope.h" + +#include "jsapi.h" +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "js/experimental/CTypes.h" // JS::InitCTypesClass, JS::CTypesCallbacks, JS::SetCTypesCallbacks +#include "js/MemoryFunctions.h" + +#include "nsNativeCharsetUtils.h" +#include "nsString.h" + +namespace mozilla::dom { + +namespace { + +#ifdef BUILD_CTYPES + +char* UnicodeToNative(JSContext* aCx, const char16_t* aSource, + size_t aSourceLen) { + nsDependentSubstring unicode(aSource, aSourceLen); + + nsAutoCString native; + if (NS_FAILED(NS_CopyUnicodeToNative(unicode, native))) { + JS_ReportErrorASCII(aCx, "Could not convert string to native charset!"); + return nullptr; + } + + char* result = static_cast<char*>(JS_malloc(aCx, native.Length() + 1)); + if (!result) { + return nullptr; + } + + memcpy(result, native.get(), native.Length()); + result[native.Length()] = 0; + return result; +} + +#endif // BUILD_CTYPES + +} // namespace + +bool DefineChromeWorkerFunctions(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) { + // Currently ctypes is the only special property given to ChromeWorkers. +#ifdef BUILD_CTYPES + { + JS::Rooted<JS::Value> ctypes(aCx); + if (!JS::InitCTypesClass(aCx, aGlobal) || + !JS_GetProperty(aCx, aGlobal, "ctypes", &ctypes)) { + return false; + } + + static const JS::CTypesCallbacks callbacks = {UnicodeToNative}; + + JS::SetCTypesCallbacks(ctypes.toObjectOrNull(), &callbacks); + } +#endif // BUILD_CTYPES + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/workers/ChromeWorkerScope.h b/dom/workers/ChromeWorkerScope.h new file mode 100644 index 0000000000..be415a0403 --- /dev/null +++ b/dom/workers/ChromeWorkerScope.h @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_chromeworkerscope_h__ +#define mozilla_dom_workers_chromeworkerscope_h__ + +#include "js/TypeDecls.h" + +namespace mozilla::dom { + +bool DefineChromeWorkerFunctions(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + +} // namespace mozilla::dom + +#endif // mozilla_dom_workers_chromeworkerscope_h__ diff --git a/dom/workers/EventWithOptionsRunnable.cpp b/dom/workers/EventWithOptionsRunnable.cpp new file mode 100644 index 0000000000..2ddc56d946 --- /dev/null +++ b/dom/workers/EventWithOptionsRunnable.cpp @@ -0,0 +1,160 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "EventWithOptionsRunnable.h" +#include "WorkerScope.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "js/StructuredClone.h" +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "nsJSPrincipals.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "MainThreadUtils.h" +#include "mozilla/Assertions.h" +#include "nsGlobalWindowInner.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/ErrorResult.h" +#include "nsIGlobalObject.h" +#include "nsCOMPtr.h" +#include "js/GlobalObject.h" +#include "xpcpublic.h" +#include "mozilla/dom/MessagePortBinding.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/WorkerCommon.h" + +namespace mozilla::dom { +EventWithOptionsRunnable::EventWithOptionsRunnable(Worker& aWorker, + const char* aName) + : WorkerDebuggeeRunnable(aWorker.mWorkerPrivate, aName, + WorkerRunnable::WorkerThread), + StructuredCloneHolder(CloningSupported, TransferringSupported, + StructuredCloneScope::SameProcess) {} + +EventWithOptionsRunnable::~EventWithOptionsRunnable() = default; + +void EventWithOptionsRunnable::InitOptions( + JSContext* aCx, JS::Handle<JS::Value> aOptions, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv) { + JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue()); + + aRv = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransferable, + &transferable); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + JS::CloneDataPolicy clonePolicy; + // DedicatedWorkers are always part of the same agent cluster. + clonePolicy.allowIntraClusterClonableSharedObjects(); + + MOZ_ASSERT(NS_IsMainThread()); + nsGlobalWindowInner* win = nsContentUtils::IncumbentInnerWindow(); + if (win && win->IsSharedMemoryAllowed()) { + clonePolicy.allowSharedMemoryObjects(); + } + + Write(aCx, aOptions, transferable, clonePolicy, aRv); +} + +// Cargo-culted from MesssageEventRunnable. +bool EventWithOptionsRunnable::BuildAndFireEvent( + JSContext* aCx, WorkerPrivate* aWorkerPrivate, + DOMEventTargetHelper* aTarget) { + IgnoredErrorResult rv; + nsCOMPtr<nsIGlobalObject> parent = aTarget->GetParentObject(); + + // For some workers without window, parent is null and we try to find it + // from the JS Context. + if (!parent) { + JS::Rooted<JSObject*> globalObject(aCx, JS::CurrentGlobalOrNull(aCx)); + if (NS_WARN_IF(!globalObject)) { + rv.ThrowDataCloneError("failed to get global object"); + OptionsDeserializeFailed(rv); + return false; + } + + parent = xpc::NativeGlobal(globalObject); + if (NS_WARN_IF(!parent)) { + rv.ThrowDataCloneError("failed to get parent"); + OptionsDeserializeFailed(rv); + return false; + } + } + + MOZ_ASSERT(parent); + + JS::Rooted<JS::Value> options(aCx); + + JS::CloneDataPolicy cloneDataPolicy; + if (parent->GetClientInfo().isSome() && + parent->GetClientInfo()->AgentClusterId().isSome() && + parent->GetClientInfo()->AgentClusterId()->Equals( + aWorkerPrivate->AgentClusterId())) { + cloneDataPolicy.allowIntraClusterClonableSharedObjects(); + } + + if (aWorkerPrivate->IsSharedMemoryAllowed()) { + cloneDataPolicy.allowSharedMemoryObjects(); + } + + Read(parent, aCx, &options, cloneDataPolicy, rv); + + if (NS_WARN_IF(rv.Failed())) { + OptionsDeserializeFailed(rv); + return false; + } + + Sequence<OwningNonNull<MessagePort>> ports; + if (NS_WARN_IF(!TakeTransferredPortsAsSequence(ports))) { + // TODO: Is this an appropriate type? What does this actually do? + rv.ThrowDataCloneError("TakeTransferredPortsAsSequence failed"); + OptionsDeserializeFailed(rv); + return false; + } + + RefPtr<dom::Event> event = BuildEvent(aCx, parent, aTarget, options); + + if (NS_WARN_IF(!event)) { + return false; + } + + aTarget->DispatchEvent(*event); + return true; +} + +bool EventWithOptionsRunnable::WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) { + if (mTarget == ParentThread) { + // Don't fire this event if the JS object has been disconnected from the + // private object. + if (!aWorkerPrivate->IsAcceptingEvents()) { + return true; + } + + aWorkerPrivate->AssertInnerWindowIsCorrect(); + + return BuildAndFireEvent(aCx, aWorkerPrivate, + aWorkerPrivate->ParentEventTargetRef()); + } + + MOZ_ASSERT(aWorkerPrivate == GetWorkerPrivateFromContext(aCx)); + MOZ_ASSERT(aWorkerPrivate->GlobalScope()); + + // If the worker start shutting down, don't dispatch the event. + if (NS_FAILED( + aWorkerPrivate->GlobalScope()->CheckCurrentGlobalCorrectness())) { + return true; + } + + return BuildAndFireEvent(aCx, aWorkerPrivate, aWorkerPrivate->GlobalScope()); +} + +} // namespace mozilla::dom diff --git a/dom/workers/EventWithOptionsRunnable.h b/dom/workers/EventWithOptionsRunnable.h new file mode 100644 index 0000000000..de228f2688 --- /dev/null +++ b/dom/workers/EventWithOptionsRunnable.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_DOM_WORKERS_EVENTWITHOPTIONSRUNNABLE_H_ +#define MOZILLA_DOM_WORKERS_EVENTWITHOPTIONSRUNNABLE_H_ + +#include "WorkerCommon.h" +#include "WorkerRunnable.h" +#include "mozilla/dom/StructuredCloneHolder.h" + +namespace mozilla { +class DOMEventTargetHelper; + +namespace dom { +class Event; +class EventTarget; +class Worker; +class WorkerPrivate; + +// Cargo-culted from MesssageEventRunnable. +// Intended to be used for the idiom where arbitrary options are transferred to +// the worker thread (with optional transfer functions), which are then used to +// build an event, which is then fired on the global worker scope. +class EventWithOptionsRunnable : public WorkerDebuggeeRunnable, + public StructuredCloneHolder { + public: + explicit EventWithOptionsRunnable( + Worker& aWorker, const char* aName = "EventWithOptionsRunnable"); + void InitOptions(JSContext* aCx, JS::Handle<JS::Value> aOptions, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv); + + // Called on the worker thread. The event returned will be fired on the + // worker's global scope. If a StrongWorkerRef needs to be retained, the + // implementation can do so with the WorkerPrivate. + virtual already_AddRefed<Event> BuildEvent( + JSContext* aCx, nsIGlobalObject* aGlobal, EventTarget* aTarget, + JS::Handle<JS::Value> aOptions) = 0; + + // Called on the worker thread + virtual void OptionsDeserializeFailed(ErrorResult& aRv) {} + + protected: + virtual ~EventWithOptionsRunnable(); + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + bool BuildAndFireEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + DOMEventTargetHelper* aTarget); +}; +} // namespace dom +} // namespace mozilla + +#endif // MOZILLA_DOM_WORKERS_EVENTWITHOPTIONSRUNNABLE_H_ diff --git a/dom/workers/JSExecutionManager.cpp b/dom/workers/JSExecutionManager.cpp new file mode 100644 index 0000000000..9482888423 --- /dev/null +++ b/dom/workers/JSExecutionManager.cpp @@ -0,0 +1,251 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/JSExecutionManager.h" + +#include "WorkerCommon.h" +#include "WorkerPrivate.h" + +#include "mozilla/dom/DocGroup.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla::dom { + +JSExecutionManager* JSExecutionManager::mCurrentMTManager; + +const uint32_t kTimeSliceExpirationMS = 50; + +using RequestState = JSExecutionManager::RequestState; + +static StaticRefPtr<JSExecutionManager> sSABSerializationManager; + +void JSExecutionManager::Initialize() { + if (StaticPrefs::dom_workers_serialized_sab_access()) { + sSABSerializationManager = MakeRefPtr<JSExecutionManager>(1); + } +} + +void JSExecutionManager::Shutdown() { sSABSerializationManager = nullptr; } + +JSExecutionManager* JSExecutionManager::GetSABSerializationManager() { + return sSABSerializationManager.get(); +} + +RequestState JSExecutionManager::RequestJSThreadExecution() { + if (NS_IsMainThread()) { + return RequestJSThreadExecutionMainThread(); + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + + if (!workerPrivate || workerPrivate->GetExecutionGranted()) { + return RequestState::ExecutingAlready; + } + + MutexAutoLock lock(mExecutionQueueMutex); + MOZ_ASSERT(mMaxRunning >= mRunning); + + if ((mExecutionQueue.size() + (mMainThreadAwaitingExecution ? 1 : 0)) < + size_t(mMaxRunning - mRunning)) { + // There's slots ready for things to run, execute right away. + workerPrivate->SetExecutionGranted(true); + workerPrivate->ScheduleTimeSliceExpiration(kTimeSliceExpirationMS); + + mRunning++; + return RequestState::Granted; + } + + mExecutionQueue.push_back(workerPrivate); + + TimeStamp waitStart = TimeStamp::Now(); + + while (mRunning >= mMaxRunning || (workerPrivate != mExecutionQueue.front() || + mMainThreadAwaitingExecution)) { + // If there is no slots available, the main thread is awaiting permission + // or we are not first in line for execution, wait until notified. + mExecutionQueueCondVar.Wait(TimeDuration::FromMilliseconds(500)); + if ((TimeStamp::Now() - waitStart) > TimeDuration::FromSeconds(20)) { + // Crash so that these types of situations are actually caught in the + // crash reporter. + MOZ_CRASH(); + } + } + + workerPrivate->SetExecutionGranted(true); + workerPrivate->ScheduleTimeSliceExpiration(kTimeSliceExpirationMS); + + mExecutionQueue.pop_front(); + mRunning++; + if (mRunning < mMaxRunning) { + // If a thread woke up before that wasn't first in line it will have gone + // back to sleep, if there's more slots available, wake it now. + mExecutionQueueCondVar.NotifyAll(); + } + + return RequestState::Granted; +} + +void JSExecutionManager::YieldJSThreadExecution() { + if (NS_IsMainThread()) { + MOZ_ASSERT(mMainThreadIsExecuting); + mMainThreadIsExecuting = false; + + MutexAutoLock lock(mExecutionQueueMutex); + mRunning--; + mExecutionQueueCondVar.NotifyAll(); + return; + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + + if (!workerPrivate) { + return; + } + + MOZ_ASSERT(workerPrivate->GetExecutionGranted()); + + workerPrivate->CancelTimeSliceExpiration(); + + MutexAutoLock lock(mExecutionQueueMutex); + mRunning--; + mExecutionQueueCondVar.NotifyAll(); + workerPrivate->SetExecutionGranted(false); +} + +bool JSExecutionManager::YieldJSThreadExecutionIfGranted() { + if (NS_IsMainThread()) { + if (mMainThreadIsExecuting) { + YieldJSThreadExecution(); + return true; + } + return false; + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + + if (workerPrivate && workerPrivate->GetExecutionGranted()) { + YieldJSThreadExecution(); + return true; + } + + return false; +} + +RequestState JSExecutionManager::RequestJSThreadExecutionMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mMainThreadIsExecuting) { + return RequestState::ExecutingAlready; + } + + MutexAutoLock lock(mExecutionQueueMutex); + MOZ_ASSERT(mMaxRunning >= mRunning); + + if ((mMaxRunning - mRunning) > 0) { + // If there's any slots available run, the main thread always takes + // precedence over any worker threads. + mRunning++; + mMainThreadIsExecuting = true; + return RequestState::Granted; + } + + mMainThreadAwaitingExecution = true; + + TimeStamp waitStart = TimeStamp::Now(); + + while (mRunning >= mMaxRunning) { + if ((TimeStamp::Now() - waitStart) > TimeDuration::FromSeconds(20)) { + // Crash so that these types of situations are actually caught in the + // crash reporter. + MOZ_CRASH(); + } + mExecutionQueueCondVar.Wait(TimeDuration::FromMilliseconds(500)); + } + + mMainThreadAwaitingExecution = false; + mMainThreadIsExecuting = true; + + mRunning++; + if (mRunning < mMaxRunning) { + // If a thread woke up before that wasn't first in line it will have gone + // back to sleep, if there's more slots available, wake it now. + mExecutionQueueCondVar.NotifyAll(); + } + + return RequestState::Granted; +} + +AutoRequestJSThreadExecution::AutoRequestJSThreadExecution( + nsIGlobalObject* aGlobalObject, bool aIsMainThread) { + JSExecutionManager* manager = nullptr; + + mIsMainThread = aIsMainThread; + if (mIsMainThread) { + mOldGrantingManager = JSExecutionManager::mCurrentMTManager; + + nsPIDOMWindowInner* innerWindow = nullptr; + if (aGlobalObject) { + innerWindow = aGlobalObject->GetAsInnerWindow(); + } + + DocGroup* docGroup = nullptr; + if (innerWindow) { + docGroup = innerWindow->GetDocGroup(); + } + + if (docGroup) { + manager = docGroup->GetExecutionManager(); + } + + if (JSExecutionManager::mCurrentMTManager == manager) { + return; + } + + if (JSExecutionManager::mCurrentMTManager) { + JSExecutionManager::mCurrentMTManager->YieldJSThreadExecution(); + JSExecutionManager::mCurrentMTManager = nullptr; + } + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + + if (workerPrivate) { + manager = workerPrivate->GetExecutionManager(); + } + } + + if (manager && + (manager->RequestJSThreadExecution() == RequestState::Granted)) { + if (NS_IsMainThread()) { + // Make sure we restore permission on destruction if needed. + JSExecutionManager::mCurrentMTManager = manager; + } + mExecutionGrantingManager = std::move(manager); + } +} + +AutoYieldJSThreadExecution::AutoYieldJSThreadExecution() { + JSExecutionManager* manager = nullptr; + + if (NS_IsMainThread()) { + manager = JSExecutionManager::mCurrentMTManager; + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + + if (workerPrivate) { + manager = workerPrivate->GetExecutionManager(); + } + } + + if (manager && manager->YieldJSThreadExecutionIfGranted()) { + mExecutionGrantingManager = std::move(manager); + if (NS_IsMainThread()) { + JSExecutionManager::mCurrentMTManager = nullptr; + } + } +} + +} // namespace mozilla::dom diff --git a/dom/workers/JSExecutionManager.h b/dom/workers/JSExecutionManager.h new file mode 100644 index 0000000000..132884bf53 --- /dev/null +++ b/dom/workers/JSExecutionManager.h @@ -0,0 +1,192 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_jsexecutionmanager_h__ +#define mozilla_dom_workers_jsexecutionmanager_h__ + +#include <stdint.h> +#include <deque> +#include "MainThreadUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CondVar.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "nsISupports.h" + +class nsIGlobalObject; +namespace mozilla { + +class ErrorResult; + +namespace dom { +class WorkerPrivate; + +// The code in this file is responsible for throttling JS execution. It does +// this by introducing a JSExecutionManager class. An execution manager may be +// applied to any number of worker threads or a DocGroup on the main thread. +// +// JS environments associated with a JS execution manager may only execute on a +// certain amount of CPU cores in parallel. +// +// Whenever the main thread, or a worker thread begins executing JS it should +// make sure AutoRequestJSThreadExecution is present on the stack, in practice +// this is done by it being part of AutoEntryScript. +// +// Whenever the main thread may end up blocking on the activity of a worker +// thread, it should make sure to have an AutoYieldJSThreadExecution object +// on the stack. +// +// Whenever a worker thread may end up blocking on the main thread or the +// activity of another worker thread, it should make sure to have an +// AutoYieldJSThreadExecution object on the stack. +// +// Failure to do this may result in a deadlock. When a deadlock occurs due +// to these circumstances we will crash after 20 seconds. +// +// For the main thread this class should only be used in the case of an +// emergency surrounding exploitability of SharedArrayBuffers. A single +// execution manager will then be shared between all Workers and the main +// thread doc group containing the SharedArrayBuffer and ensure all this code +// only runs in a serialized manner. On the main thread we therefore may have +// 1 execution manager per DocGroup, as this is the granularity at which +// SharedArrayBuffers may be present. + +class AutoRequestJSThreadExecution; +class AutoYieldJSThreadExecution; + +// This class is used to regulate JS execution when for whatever reason we wish +// to throttle execution of multiple JS environments to occur with a certain +// maximum of synchronously executing threads. This should be used through +// the stack helper classes. +class JSExecutionManager { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JSExecutionManager) + + explicit JSExecutionManager(int32_t aMaxRunning = 1) + : mMaxRunning(aMaxRunning) {} + + enum class RequestState { Granted, ExecutingAlready }; + + static void Initialize(); + static void Shutdown(); + + static JSExecutionManager* GetSABSerializationManager(); + + private: + friend class AutoRequestJSThreadExecution; + friend class AutoYieldJSThreadExecution; + ~JSExecutionManager() = default; + + // Methods used by Auto*JSThreadExecution + + // Request execution permission, returns ExecutingAlready if execution was + // already granted or does not apply to this thread. + RequestState RequestJSThreadExecution(); + + // Yield JS execution, this asserts that permission is actually granted. + void YieldJSThreadExecution(); + + // Yield JS execution if permission was granted. This returns false if no + // permission is granted. This method is needed because an execution manager + // may have been set in between the ctor and dtor of + // AutoYieldJSThreadExecution. + bool YieldJSThreadExecutionIfGranted(); + + RequestState RequestJSThreadExecutionMainThread(); + + // Execution manager currently managing the main thread. + // MainThread access only. + static JSExecutionManager* mCurrentMTManager; + + // Workers waiting to be given permission for execution. + // Guarded by mExecutionQueueMutex. + std::deque<WorkerPrivate*> mExecutionQueue + MOZ_GUARDED_BY(mExecutionQueueMutex); + + // Number of threads currently executing concurrently for this manager. + // Guarded by mExecutionQueueMutex. + int32_t mRunning MOZ_GUARDED_BY(mExecutionQueueMutex) = 0; + + // Number of threads allowed to run concurrently for environments managed + // by this manager. + // Guarded by mExecutionQueueMutex. + int32_t mMaxRunning MOZ_GUARDED_BY(mExecutionQueueMutex) = 1; + + // Mutex that guards the execution queue and associated state. + Mutex mExecutionQueueMutex = + Mutex{"JSExecutionManager::sExecutionQueueMutex"}; + + // ConditionVariables that blocked threads wait for. + CondVar mExecutionQueueCondVar = + CondVar{mExecutionQueueMutex, "JSExecutionManager::sExecutionQueueMutex"}; + + // Whether the main thread is currently executing for this manager. + // MainThread access only. + bool mMainThreadIsExecuting = false; + + // Whether the main thread is currently awaiting permission to execute. Main + // thread execution is always prioritized. + // Guarded by mExecutionQueueMutex. + bool mMainThreadAwaitingExecution MOZ_GUARDED_BY(mExecutionQueueMutex) = + false; +}; + +// Helper for managing execution requests and allowing re-entrant permission +// requests. +class MOZ_STACK_CLASS AutoRequestJSThreadExecution { + public: + explicit AutoRequestJSThreadExecution(nsIGlobalObject* aGlobalObject, + bool aIsMainThread); + + ~AutoRequestJSThreadExecution() { + if (mExecutionGrantingManager) { + mExecutionGrantingManager->YieldJSThreadExecution(); + } + if (mIsMainThread) { + if (mOldGrantingManager) { + mOldGrantingManager->RequestJSThreadExecution(); + } + JSExecutionManager::mCurrentMTManager = mOldGrantingManager; + } + } + + private: + // The manager we obtained permission from. nullptr if permission was already + // granted. + RefPtr<JSExecutionManager> mExecutionGrantingManager; + // The manager we had permission from before, and where permission should be + // re-requested upon destruction. + RefPtr<JSExecutionManager> mOldGrantingManager; + + // We store this for performance reasons. + bool mIsMainThread; +}; + +// Class used to wrap code which essentially exits JS execution and may block +// on other threads. +class MOZ_STACK_CLASS AutoYieldJSThreadExecution { + public: + AutoYieldJSThreadExecution(); + + ~AutoYieldJSThreadExecution() { + if (mExecutionGrantingManager) { + mExecutionGrantingManager->RequestJSThreadExecution(); + if (NS_IsMainThread()) { + JSExecutionManager::mCurrentMTManager = mExecutionGrantingManager; + } + } + } + + private: + // Set to the granting manager if we were granted permission here. + RefPtr<JSExecutionManager> mExecutionGrantingManager; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_jsexecutionmanager_h__ diff --git a/dom/workers/JSSettings.h b/dom/workers/JSSettings.h new file mode 100644 index 0000000000..af2247a847 --- /dev/null +++ b/dom/workers/JSSettings.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workerinternals_JSSettings_h +#define mozilla_dom_workerinternals_JSSettings_h + +#include <stdint.h> + +#include "js/ContextOptions.h" +#include "js/GCAPI.h" +#include "js/RealmOptions.h" +#include "mozilla/Maybe.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla::dom::workerinternals { + +// Random unique constant to facilitate JSPrincipal debugging +const uint32_t kJSPrincipalsDebugToken = 0x7e2df9d2; + +struct JSSettings { + struct JSGCSetting { + JSGCParamKey key; + // Nothing() represents the default value, the result of calling + // JS_ResetGCParameter. + Maybe<uint32_t> value; + + // For the IndexOf call below. + bool operator==(JSGCParamKey k) const { return key == k; } + }; + + JS::RealmOptions chromeRealmOptions; + JS::RealmOptions contentRealmOptions; + CopyableTArray<JSGCSetting> gcSettings; + JS::ContextOptions contextOptions; + +#ifdef JS_GC_ZEAL + uint8_t gcZeal = 0; + uint32_t gcZealFrequency = 0; +#endif + + // Returns whether there was a change in the setting. + bool ApplyGCSetting(JSGCParamKey aKey, Maybe<uint32_t> aValue) { + size_t index = gcSettings.IndexOf(aKey); + if (index == gcSettings.NoIndex) { + gcSettings.AppendElement(JSGCSetting{aKey, aValue}); + return true; + } + if (gcSettings[index].value != aValue) { + gcSettings[index].value = aValue; + return true; + } + return false; + } +}; + +} // namespace mozilla::dom::workerinternals + +#endif // mozilla_dom_workerinternals_JSSettings_h diff --git a/dom/workers/MessageEventRunnable.cpp b/dom/workers/MessageEventRunnable.cpp new file mode 100644 index 0000000000..8edc7037cd --- /dev/null +++ b/dom/workers/MessageEventRunnable.cpp @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MessageEventRunnable.h" + +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/RootedDictionary.h" +#include "nsQueryObject.h" +#include "WorkerScope.h" + +namespace mozilla::dom { + +MessageEventRunnable::MessageEventRunnable(WorkerPrivate* aWorkerPrivate, + Target aTarget) + : WorkerDebuggeeRunnable(aWorkerPrivate, "MessageEventRunnable", aTarget), + StructuredCloneHolder(CloningSupported, TransferringSupported, + StructuredCloneScope::SameProcess) {} + +bool MessageEventRunnable::DispatchDOMEvent(JSContext* aCx, + WorkerPrivate* aWorkerPrivate, + DOMEventTargetHelper* aTarget, + bool aIsMainThread) { + nsCOMPtr<nsIGlobalObject> parent = aTarget->GetParentObject(); + + // For some workers without window, parent is null and we try to find it + // from the JS Context. + if (!parent) { + JS::Rooted<JSObject*> globalObject(aCx, JS::CurrentGlobalOrNull(aCx)); + if (NS_WARN_IF(!globalObject)) { + return false; + } + + parent = xpc::NativeGlobal(globalObject); + if (NS_WARN_IF(!parent)) { + return false; + } + } + + MOZ_ASSERT(parent); + + JS::Rooted<JS::Value> messageData(aCx); + IgnoredErrorResult rv; + + JS::CloneDataPolicy cloneDataPolicy; + if (parent->GetClientInfo().isSome() && + parent->GetClientInfo()->AgentClusterId().isSome() && + parent->GetClientInfo()->AgentClusterId()->Equals( + aWorkerPrivate->AgentClusterId())) { + cloneDataPolicy.allowIntraClusterClonableSharedObjects(); + } + + if (aWorkerPrivate->IsSharedMemoryAllowed()) { + cloneDataPolicy.allowSharedMemoryObjects(); + } + + Read(parent, aCx, &messageData, cloneDataPolicy, rv); + + if (NS_WARN_IF(rv.Failed())) { + DispatchError(aCx, aTarget); + return false; + } + + Sequence<OwningNonNull<MessagePort>> ports; + if (!TakeTransferredPortsAsSequence(ports)) { + DispatchError(aCx, aTarget); + return false; + } + + RefPtr<MessageEvent> event = new MessageEvent(aTarget, nullptr, nullptr); + event->InitMessageEvent(nullptr, u"message"_ns, CanBubble::eNo, + Cancelable::eNo, messageData, u""_ns, u""_ns, nullptr, + ports); + + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); + + return true; +} + +bool MessageEventRunnable::WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) { + if (mTarget == ParentThread) { + // Don't fire this event if the JS object has been disconnected from the + // private object. + if (!aWorkerPrivate->IsAcceptingEvents()) { + return true; + } + + aWorkerPrivate->AssertInnerWindowIsCorrect(); + + return DispatchDOMEvent(aCx, aWorkerPrivate, + aWorkerPrivate->ParentEventTargetRef(), + !aWorkerPrivate->GetParent()); + } + + MOZ_ASSERT(aWorkerPrivate == GetWorkerPrivateFromContext(aCx)); + MOZ_ASSERT(aWorkerPrivate->GlobalScope()); + + // If the worker start shutting down, don't dispatch the message event. + if (NS_FAILED( + aWorkerPrivate->GlobalScope()->CheckCurrentGlobalCorrectness())) { + return true; + } + + return DispatchDOMEvent(aCx, aWorkerPrivate, aWorkerPrivate->GlobalScope(), + false); +} + +void MessageEventRunnable::DispatchError(JSContext* aCx, + DOMEventTargetHelper* aTarget) { + RootedDictionary<MessageEventInit> init(aCx); + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<Event> event = + MessageEvent::Constructor(aTarget, u"messageerror"_ns, init); + event->SetTrusted(true); + + aTarget->DispatchEvent(*event); +} + +} // namespace mozilla::dom diff --git a/dom/workers/MessageEventRunnable.h b/dom/workers/MessageEventRunnable.h new file mode 100644 index 0000000000..c20efb4c55 --- /dev/null +++ b/dom/workers/MessageEventRunnable.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_MessageEventRunnable_h +#define mozilla_dom_workers_MessageEventRunnable_h + +#include "WorkerCommon.h" +#include "WorkerRunnable.h" +#include "mozilla/dom/StructuredCloneHolder.h" + +namespace mozilla { + +class DOMEventTargetHelper; + +namespace dom { + +class MessageEventRunnable final : public WorkerDebuggeeRunnable, + public StructuredCloneHolder { + public: + MessageEventRunnable(WorkerPrivate* aWorkerPrivate, Target aTarget); + + bool DispatchDOMEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + DOMEventTargetHelper* aTarget, bool aIsMainThread); + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + + void DispatchError(JSContext* aCx, DOMEventTargetHelper* aTarget); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_MessageEventRunnable_h diff --git a/dom/workers/Queue.h b/dom/workers/Queue.h new file mode 100644 index 0000000000..cf65932698 --- /dev/null +++ b/dom/workers/Queue.h @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workerinternal_Queue_h +#define mozilla_dom_workerinternal_Queue_h + +#include "mozilla/Mutex.h" +#include "nsTArray.h" + +namespace mozilla::dom::workerinternals { + +template <typename T, int TCount> +struct StorageWithTArray { + typedef AutoTArray<T, TCount> StorageType; + + static void Reverse(StorageType& aStorage) { + uint32_t length = aStorage.Length(); + for (uint32_t index = 0; index < length / 2; index++) { + uint32_t reverseIndex = length - 1 - index; + + T t1 = aStorage.ElementAt(index); + T t2 = aStorage.ElementAt(reverseIndex); + + aStorage.ReplaceElementsAt(index, 1, t2); + aStorage.ReplaceElementsAt(reverseIndex, 1, t1); + } + } + + static bool IsEmpty(const StorageType& aStorage) { + return !!aStorage.IsEmpty(); + } + + static void Push(StorageType& aStorage, const T& aEntry) { + aStorage.AppendElement(aEntry); + } + + static bool Pop(StorageType& aStorage, T& aEntry) { + if (IsEmpty(aStorage)) { + return false; + } + + aEntry = aStorage.PopLastElement(); + return true; + } + + static void Clear(StorageType& aStorage) { aStorage.Clear(); } + + static void Compact(StorageType& aStorage) { aStorage.Compact(); } +}; + +class MOZ_CAPABILITY("mutex") LockingWithMutex { + mozilla::Mutex mMutex; + + protected: + LockingWithMutex() : mMutex("LockingWithMutex::mMutex") {} + + void Lock() MOZ_CAPABILITY_ACQUIRE() { mMutex.Lock(); } + + void Unlock() MOZ_CAPABILITY_RELEASE() { mMutex.Unlock(); } + + class MOZ_SCOPED_CAPABILITY AutoLock { + LockingWithMutex& mHost; + + public: + explicit AutoLock(LockingWithMutex& aHost) MOZ_CAPABILITY_ACQUIRE(aHost) + : mHost(aHost) { + mHost.Lock(); + } + + ~AutoLock() MOZ_CAPABILITY_RELEASE() { mHost.Unlock(); } + }; + + friend class AutoLock; +}; + +class NoLocking { + protected: + void Lock() {} + + void Unlock() {} + + class AutoLock { + public: + explicit AutoLock(NoLocking& aHost) {} + + ~AutoLock() = default; + }; +}; + +template <typename T, int TCount = 256, class LockingPolicy = NoLocking, + class StoragePolicy = + StorageWithTArray<T, TCount % 2 ? TCount / 2 + 1 : TCount / 2> > +class Queue : public LockingPolicy { + typedef typename StoragePolicy::StorageType StorageType; + typedef typename LockingPolicy::AutoLock AutoLock; + + StorageType mStorage1; + StorageType mStorage2; + + StorageType* mFront; + StorageType* mBack; + + public: + Queue() : mFront(&mStorage1), mBack(&mStorage2) {} + + bool IsEmpty() { + AutoLock lock(*this); + return StoragePolicy::IsEmpty(*mFront) && StoragePolicy::IsEmpty(*mBack); + } + + void Push(const T& aEntry) { + AutoLock lock(*this); + StoragePolicy::Push(*mBack, aEntry); + } + + bool Pop(T& aEntry) { + AutoLock lock(*this); + if (StoragePolicy::IsEmpty(*mFront)) { + StoragePolicy::Compact(*mFront); + StoragePolicy::Reverse(*mBack); + StorageType* tmp = mFront; + mFront = mBack; + mBack = tmp; + } + return StoragePolicy::Pop(*mFront, aEntry); + } + + void Clear() { + AutoLock lock(*this); + StoragePolicy::Clear(*mFront); + StoragePolicy::Clear(*mBack); + } + + // XXX Do we need this? + void Lock() { LockingPolicy::Lock(); } + + // XXX Do we need this? + void Unlock() { LockingPolicy::Unlock(); } + + private: + // Queue is not copyable. + Queue(const Queue&); + Queue& operator=(const Queue&); +}; + +} // namespace mozilla::dom::workerinternals + +#endif /* mozilla_dom_workerinternals_Queue_h*/ diff --git a/dom/workers/RegisterBindings.cpp b/dom/workers/RegisterBindings.cpp new file mode 100644 index 0000000000..4c33b1e4c5 --- /dev/null +++ b/dom/workers/RegisterBindings.cpp @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerPrivate.h" +#include "ChromeWorkerScope.h" + +#include "jsapi.h" +#include "mozilla/dom/DebuggerNotificationObserverBinding.h" +#include "mozilla/dom/RegisterWorkerBindings.h" +#include "mozilla/dom/RegisterWorkerDebuggerBindings.h" + +using namespace mozilla::dom; + +bool WorkerPrivate::RegisterBindings(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) { + // Init Web IDL bindings + if (!RegisterWorkerBindings(aCx, aGlobal)) { + return false; + } + + if (IsChromeWorker()) { + if (!DefineChromeWorkerFunctions(aCx, aGlobal)) { + return false; + } + } + + return true; +} + +bool WorkerPrivate::RegisterDebuggerBindings(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) { + // Init Web IDL bindings + if (!RegisterWorkerDebuggerBindings(aCx, aGlobal)) { + return false; + } + + if (!ChromeUtils_Binding::GetConstructorObject(aCx)) { + return false; + } + + if (!DebuggerNotificationObserver_Binding::GetConstructorObject(aCx)) { + return false; + } + + if (!JS_DefineDebuggerObject(aCx, aGlobal)) { + return false; + } + + return true; +} diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp new file mode 100644 index 0000000000..3d6a883867 --- /dev/null +++ b/dom/workers/RuntimeService.cpp @@ -0,0 +1,2373 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RuntimeService.h" + +#include "nsContentSecurityUtils.h" +#include "nsIContentSecurityPolicy.h" +#include "mozilla/dom/Document.h" +#include "nsIObserverService.h" +#include "nsIScriptContext.h" +#include "nsIStreamTransportService.h" +#include "nsISupportsPriority.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIXULRuntime.h" +#include "nsPIDOMWindow.h" + +#include <algorithm> +#include "mozilla/ipc/BackgroundChild.h" +#include "GeckoProfiler.h" +#include "js/ColumnNumber.h" // JS::ColumnNumberOneOrigin +#include "js/experimental/CTypes.h" // JS::CTypesActivityType, JS::SetCTypesActivityCallback +#include "jsfriendapi.h" +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/ContextOptions.h" +#include "js/Initialization.h" +#include "js/LocaleSensitive.h" +#include "js/WasmFeatures.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/AtomList.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/EventTargetBinding.h" +#include "mozilla/dom/FetchUtil.h" +#include "mozilla/dom/MessageChannel.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/PerformanceService.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ShadowRealmGlobalScope.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/Monitor.h" +#include "nsContentUtils.h" +#include "nsCycleCollector.h" +#include "nsDOMJSUtils.h" +#include "nsISupportsImpl.h" +#include "nsLayoutStatics.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsXPCOM.h" +#include "nsXPCOMPrivate.h" +#include "xpcpublic.h" +#include "XPCSelfHostedShmem.h" + +#if defined(XP_MACOSX) +# include "nsMacUtilsImpl.h" +#endif + +#include "WorkerDebuggerManager.h" +#include "WorkerError.h" +#include "WorkerLoadInfo.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "WorkerThread.h" +#include "prsystem.h" + +#ifdef DEBUG +# include "nsICookieJarSettings.h" +#endif + +#define WORKERS_SHUTDOWN_TOPIC "web-workers-shutdown" + +static mozilla::LazyLogModule gWorkerShutdownDumpLog("WorkerShutdownDump"); + +#ifdef SHUTDOWN_LOG +# undef SHUTDOWN_LOG +#endif +#define SHUTDOWN_LOG(msg) MOZ_LOG(gWorkerShutdownDumpLog, LogLevel::Debug, msg); + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +using namespace workerinternals; + +namespace workerinternals { + +// The size of the worker runtime heaps in bytes. May be changed via pref. +#define WORKER_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024 + +// The size of the worker JS allocation threshold in MB. May be changed via +// pref. +#define WORKER_DEFAULT_ALLOCATION_THRESHOLD 30 + +// Half the size of the actual C stack, to be safe. +#define WORKER_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024 + +// The maximum number of threads to use for workers, overridable via pref. +#define MAX_WORKERS_PER_DOMAIN 512 + +static_assert(MAX_WORKERS_PER_DOMAIN >= 1, + "We should allow at least one worker per domain."); + +#define PREF_WORKERS_PREFIX "dom.workers." +#define PREF_WORKERS_MAX_PER_DOMAIN PREF_WORKERS_PREFIX "maxPerDomain" + +#define GC_REQUEST_OBSERVER_TOPIC "child-gc-request" +#define CC_REQUEST_OBSERVER_TOPIC "child-cc-request" +#define MEMORY_PRESSURE_OBSERVER_TOPIC "memory-pressure" +#define LOW_MEMORY_DATA "low-memory" +#define LOW_MEMORY_ONGOING_DATA "low-memory-ongoing" +#define MEMORY_PRESSURE_STOP_OBSERVER_TOPIC "memory-pressure-stop" + +// Prefixes for observing preference changes. +#define PREF_JS_OPTIONS_PREFIX "javascript.options." +#define PREF_MEM_OPTIONS_PREFIX "mem." +#define PREF_GCZEAL "gczeal" + +static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID); + +namespace { + +const uint32_t kNoIndex = uint32_t(-1); + +uint32_t gMaxWorkersPerDomain = MAX_WORKERS_PER_DOMAIN; + +// Does not hold an owning reference. +Atomic<RuntimeService*> gRuntimeService(nullptr); + +// Only true during the call to Init. +bool gRuntimeServiceDuringInit = false; + +class LiteralRebindingCString : public nsDependentCString { + public: + template <int N> + void RebindLiteral(const char (&aStr)[N]) { + Rebind(aStr, N - 1); + } +}; + +template <typename T> +struct PrefTraits; + +template <> +struct PrefTraits<bool> { + using PrefValueType = bool; + + static inline PrefValueType Get(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetBool(aPref); + } + + static inline bool Exists(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetType(aPref) == nsIPrefBranch::PREF_BOOL; + } +}; + +template <> +struct PrefTraits<int32_t> { + using PrefValueType = int32_t; + + static inline PrefValueType Get(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetInt(aPref); + } + + static inline bool Exists(const char* aPref) { + AssertIsOnMainThread(); + return Preferences::GetType(aPref) == nsIPrefBranch::PREF_INT; + } +}; + +template <typename T> +T GetPref(const char* aFullPref, const T aDefault, bool* aPresent = nullptr) { + AssertIsOnMainThread(); + + using PrefHelper = PrefTraits<T>; + + T result; + bool present = true; + + if (PrefHelper::Exists(aFullPref)) { + result = PrefHelper::Get(aFullPref); + } else { + result = aDefault; + present = false; + } + + if (aPresent) { + *aPresent = present; + } + return result; +} + +void LoadContextOptions(const char* aPrefName, void* /* aClosure */) { + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + if (!rts) { + // May be shutting down, just bail. + return; + } + + const nsDependentCString prefName(aPrefName); + + // Several other pref branches will get included here so bail out if there is + // another callback that will handle this change. + if (StringBeginsWith( + prefName, + nsLiteralCString(PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX))) { + return; + } + +#ifdef JS_GC_ZEAL + if (prefName.EqualsLiteral(PREF_JS_OPTIONS_PREFIX PREF_GCZEAL)) { + return; + } +#endif + + JS::ContextOptions contextOptions; + xpc::SetPrefableContextOptions(contextOptions); + + nsCOMPtr<nsIXULRuntime> xr = do_GetService("@mozilla.org/xre/runtime;1"); + if (xr) { + bool safeMode = false; + xr->GetInSafeMode(&safeMode); + if (safeMode) { + contextOptions.disableOptionsForSafeMode(); + } + } + + RuntimeService::SetDefaultContextOptions(contextOptions); + + if (rts) { + rts->UpdateAllWorkerContextOptions(); + } +} + +#ifdef JS_GC_ZEAL +void LoadGCZealOptions(const char* /* aPrefName */, void* /* aClosure */) { + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + if (!rts) { + // May be shutting down, just bail. + return; + } + + int32_t gczeal = GetPref<int32_t>(PREF_JS_OPTIONS_PREFIX PREF_GCZEAL, -1); + if (gczeal < 0) { + gczeal = 0; + } + + int32_t frequency = + GetPref<int32_t>(PREF_JS_OPTIONS_PREFIX PREF_GCZEAL ".frequency", -1); + if (frequency < 0) { + frequency = JS_DEFAULT_ZEAL_FREQ; + } + + RuntimeService::SetDefaultGCZeal(uint8_t(gczeal), uint32_t(frequency)); + + if (rts) { + rts->UpdateAllWorkerGCZeal(); + } +} +#endif + +void UpdateCommonJSGCMemoryOption(RuntimeService* aRuntimeService, + const char* aPrefName, JSGCParamKey aKey) { + AssertIsOnMainThread(); + NS_ASSERTION(aPrefName, "Null pref name!"); + + int32_t prefValue = GetPref(aPrefName, -1); + Maybe<uint32_t> value = (prefValue < 0 || prefValue >= 10000) + ? Nothing() + : Some(uint32_t(prefValue)); + + RuntimeService::SetDefaultJSGCSettings(aKey, value); + + if (aRuntimeService) { + aRuntimeService->UpdateAllWorkerMemoryParameter(aKey, value); + } +} + +void UpdateOtherJSGCMemoryOption(RuntimeService* aRuntimeService, + JSGCParamKey aKey, Maybe<uint32_t> aValue) { + AssertIsOnMainThread(); + + RuntimeService::SetDefaultJSGCSettings(aKey, aValue); + + if (aRuntimeService) { + aRuntimeService->UpdateAllWorkerMemoryParameter(aKey, aValue); + } +} + +void LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) { + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + + if (!rts) { + // May be shutting down, just bail. + return; + } + + constexpr auto memPrefix = + nsLiteralCString{PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX}; + const nsDependentCString fullPrefName(aPrefName); + + // Pull out the string that actually distinguishes the parameter we need to + // change. + nsDependentCSubstring memPrefName; + if (StringBeginsWith(fullPrefName, memPrefix)) { + memPrefName.Rebind(fullPrefName, memPrefix.Length()); + } else { + NS_ERROR("Unknown pref name!"); + return; + } + + struct WorkerGCPref { + nsLiteralCString memName; + const char* fullName; + JSGCParamKey key; + }; + +#define PREF(suffix_, key_) \ + { \ + nsLiteralCString(PREF_MEM_OPTIONS_PREFIX suffix_), \ + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX suffix_, key_ \ + } + constexpr WorkerGCPref kWorkerPrefs[] = { + PREF("max", JSGC_MAX_BYTES), + PREF("gc_high_frequency_time_limit_ms", JSGC_HIGH_FREQUENCY_TIME_LIMIT), + PREF("gc_low_frequency_heap_growth", JSGC_LOW_FREQUENCY_HEAP_GROWTH), + PREF("gc_high_frequency_large_heap_growth", + JSGC_HIGH_FREQUENCY_LARGE_HEAP_GROWTH), + PREF("gc_high_frequency_small_heap_growth", + JSGC_HIGH_FREQUENCY_SMALL_HEAP_GROWTH), + PREF("gc_small_heap_size_max_mb", JSGC_SMALL_HEAP_SIZE_MAX), + PREF("gc_large_heap_size_min_mb", JSGC_LARGE_HEAP_SIZE_MIN), + PREF("gc_balanced_heap_limits", JSGC_BALANCED_HEAP_LIMITS_ENABLED), + PREF("gc_heap_growth_factor", JSGC_HEAP_GROWTH_FACTOR), + PREF("gc_allocation_threshold_mb", JSGC_ALLOCATION_THRESHOLD), + PREF("gc_malloc_threshold_base_mb", JSGC_MALLOC_THRESHOLD_BASE), + PREF("gc_small_heap_incremental_limit", + JSGC_SMALL_HEAP_INCREMENTAL_LIMIT), + PREF("gc_large_heap_incremental_limit", + JSGC_LARGE_HEAP_INCREMENTAL_LIMIT), + PREF("gc_urgent_threshold_mb", JSGC_URGENT_THRESHOLD_MB), + PREF("gc_incremental_slice_ms", JSGC_SLICE_TIME_BUDGET_MS), + PREF("gc_min_empty_chunk_count", JSGC_MIN_EMPTY_CHUNK_COUNT), + PREF("gc_max_empty_chunk_count", JSGC_MAX_EMPTY_CHUNK_COUNT), + PREF("gc_compacting", JSGC_COMPACTING_ENABLED), + }; +#undef PREF + + auto pref = kWorkerPrefs; + auto end = kWorkerPrefs + ArrayLength(kWorkerPrefs); + + if (gRuntimeServiceDuringInit) { + // During init, we want to update every pref in kWorkerPrefs. + MOZ_ASSERT(memPrefName.IsEmpty(), + "Pref branch prefix only expected during init"); + } else { + // Otherwise, find the single pref that changed. + while (pref != end) { + if (pref->memName == memPrefName) { + end = pref + 1; + break; + } + ++pref; + } +#ifdef DEBUG + if (pref == end) { + nsAutoCString message("Workers don't support the '"); + message.Append(memPrefName); + message.AppendLiteral("' preference!"); + NS_WARNING(message.get()); + } +#endif + } + + while (pref != end) { + switch (pref->key) { + case JSGC_MAX_BYTES: { + int32_t prefValue = GetPref(pref->fullName, -1); + Maybe<uint32_t> value = (prefValue <= 0 || prefValue >= 0x1000) + ? Nothing() + : Some(uint32_t(prefValue) * 1024 * 1024); + UpdateOtherJSGCMemoryOption(rts, pref->key, value); + break; + } + case JSGC_SLICE_TIME_BUDGET_MS: { + int32_t prefValue = GetPref(pref->fullName, -1); + Maybe<uint32_t> value = (prefValue <= 0 || prefValue >= 100000) + ? Nothing() + : Some(uint32_t(prefValue)); + UpdateOtherJSGCMemoryOption(rts, pref->key, value); + break; + } + case JSGC_COMPACTING_ENABLED: + case JSGC_PARALLEL_MARKING_ENABLED: + case JSGC_BALANCED_HEAP_LIMITS_ENABLED: { + bool present; + bool prefValue = GetPref(pref->fullName, false, &present); + Maybe<uint32_t> value = present ? Some(prefValue ? 1 : 0) : Nothing(); + UpdateOtherJSGCMemoryOption(rts, pref->key, value); + break; + } + case JSGC_HIGH_FREQUENCY_TIME_LIMIT: + case JSGC_LOW_FREQUENCY_HEAP_GROWTH: + case JSGC_HIGH_FREQUENCY_LARGE_HEAP_GROWTH: + case JSGC_HIGH_FREQUENCY_SMALL_HEAP_GROWTH: + case JSGC_SMALL_HEAP_SIZE_MAX: + case JSGC_LARGE_HEAP_SIZE_MIN: + case JSGC_ALLOCATION_THRESHOLD: + case JSGC_MALLOC_THRESHOLD_BASE: + case JSGC_SMALL_HEAP_INCREMENTAL_LIMIT: + case JSGC_LARGE_HEAP_INCREMENTAL_LIMIT: + case JSGC_URGENT_THRESHOLD_MB: + case JSGC_MIN_EMPTY_CHUNK_COUNT: + case JSGC_MAX_EMPTY_CHUNK_COUNT: + case JSGC_HEAP_GROWTH_FACTOR: + UpdateCommonJSGCMemoryOption(rts, pref->fullName, pref->key); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown JSGCParamKey value"); + break; + } + ++pref; + } +} + +bool InterruptCallback(JSContext* aCx) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(worker); + + // Now is a good time to turn on profiling if it's pending. + PROFILER_JS_INTERRUPT_CALLBACK(); + + return worker->InterruptCallback(aCx); +} + +class LogViolationDetailsRunnable final : public WorkerMainThreadRunnable { + uint16_t mViolationType; + nsString mFileName; + uint32_t mLineNum; + uint32_t mColumnNum; + nsString mScriptSample; + + public: + LogViolationDetailsRunnable(WorkerPrivate* aWorker, uint16_t aViolationType, + const nsString& aFileName, uint32_t aLineNum, + uint32_t aColumnNum, + const nsAString& aScriptSample) + : WorkerMainThreadRunnable(aWorker, + "RuntimeService :: LogViolationDetails"_ns), + mViolationType(aViolationType), + mFileName(aFileName), + mLineNum(aLineNum), + mColumnNum(aColumnNum), + mScriptSample(aScriptSample) { + MOZ_ASSERT(aWorker); + } + + virtual bool MainThreadRun() override; + + private: + ~LogViolationDetailsRunnable() = default; +}; + +bool ContentSecurityPolicyAllows(JSContext* aCx, JS::RuntimeCode aKind, + JS::Handle<JSString*> aCode) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + worker->AssertIsOnWorkerThread(); + + bool evalOK; + bool reportViolation; + uint16_t violationType; + nsAutoJSString scriptSample; + if (aKind == JS::RuntimeCode::JS) { + if (NS_WARN_IF(!scriptSample.init(aCx, aCode))) { + JS_ClearPendingException(aCx); + return false; + } + + if (!nsContentSecurityUtils::IsEvalAllowed( + aCx, worker->UsesSystemPrincipal(), scriptSample)) { + return false; + } + + evalOK = worker->IsEvalAllowed(); + reportViolation = worker->GetReportEvalCSPViolations(); + violationType = nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL; + } else { + evalOK = worker->IsWasmEvalAllowed(); + reportViolation = worker->GetReportWasmEvalCSPViolations(); + violationType = nsIContentSecurityPolicy::VIOLATION_TYPE_WASM_EVAL; + } + + if (reportViolation) { + nsString fileName; + uint32_t lineNum = 0; + JS::ColumnNumberOneOrigin columnNum; + + JS::AutoFilename file; + if (JS::DescribeScriptedCaller(aCx, &file, &lineNum, &columnNum) && + file.get()) { + CopyUTF8toUTF16(MakeStringSpan(file.get()), fileName); + } else { + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + } + + RefPtr<LogViolationDetailsRunnable> runnable = + new LogViolationDetailsRunnable(worker, violationType, fileName, + lineNum, columnNum.oneOriginValue(), + scriptSample); + + ErrorResult rv; + runnable->Dispatch(Killing, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + } + + return evalOK; +} + +void CTypesActivityCallback(JSContext* aCx, JS::CTypesActivityType aType) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + worker->AssertIsOnWorkerThread(); + + switch (aType) { + case JS::CTypesActivityType::BeginCall: + worker->BeginCTypesCall(); + break; + + case JS::CTypesActivityType::EndCall: + worker->EndCTypesCall(); + break; + + case JS::CTypesActivityType::BeginCallback: + worker->BeginCTypesCallback(); + break; + + case JS::CTypesActivityType::EndCallback: + worker->EndCTypesCallback(); + break; + + default: + MOZ_CRASH("Unknown type flag!"); + } +} + +// JSDispatchableRunnables are WorkerRunnables used to dispatch JS::Dispatchable +// back to their worker thread. A WorkerRunnable is used for two reasons: +// +// 1. The JS::Dispatchable::run() callback may run JS so we cannot use a control +// runnable since they use async interrupts and break JS run-to-completion. +// +// 2. The DispatchToEventLoopCallback interface is *required* to fail during +// shutdown (see jsapi.h) which is exactly what WorkerRunnable::Dispatch() will +// do. Moreover, JS_DestroyContext() does *not* block on JS::Dispatchable::run +// being called, DispatchToEventLoopCallback failure is expected to happen +// during shutdown. +class JSDispatchableRunnable final : public WorkerRunnable { + JS::Dispatchable* mDispatchable; + + ~JSDispatchableRunnable() { MOZ_ASSERT(!mDispatchable); } + + // Disable the usual pre/post-dispatch thread assertions since we are + // dispatching from some random JS engine internal thread: + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // For the benefit of the destructor assert. + if (!aDispatchResult) { + mDispatchable = nullptr; + } + } + + public: + JSDispatchableRunnable(WorkerPrivate* aWorkerPrivate, + JS::Dispatchable* aDispatchable) + : WorkerRunnable(aWorkerPrivate, "JSDispatchableRunnable", + WorkerRunnable::WorkerThread), + mDispatchable(aDispatchable) { + MOZ_ASSERT(mDispatchable); + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); + MOZ_ASSERT(aCx == mWorkerPrivate->GetJSContext()); + MOZ_ASSERT(mDispatchable); + + AutoJSAPI jsapi; + jsapi.Init(); + + mDispatchable->run(mWorkerPrivate->GetJSContext(), + JS::Dispatchable::NotShuttingDown); + mDispatchable = nullptr; // mDispatchable may delete itself + + return true; + } + + nsresult Cancel() override { + MOZ_ASSERT(mDispatchable); + + AutoJSAPI jsapi; + jsapi.Init(); + + mDispatchable->run(mWorkerPrivate->GetJSContext(), + JS::Dispatchable::ShuttingDown); + mDispatchable = nullptr; // mDispatchable may delete itself + + return NS_OK; + } +}; + +static bool DispatchToEventLoop(void* aClosure, + JS::Dispatchable* aDispatchable) { + // This callback may execute either on the worker thread or a random + // JS-internal helper thread. + + // See comment at JS::InitDispatchToEventLoop() below for how we know the + // WorkerPrivate is alive. + WorkerPrivate* workerPrivate = reinterpret_cast<WorkerPrivate*>(aClosure); + + // Dispatch is expected to fail during shutdown for the reasons outlined in + // the JSDispatchableRunnable comment above. + RefPtr<JSDispatchableRunnable> r = + new JSDispatchableRunnable(workerPrivate, aDispatchable); + return r->Dispatch(); +} + +static bool ConsumeStream(JSContext* aCx, JS::Handle<JSObject*> aObj, + JS::MimeType aMimeType, + JS::StreamConsumer* aConsumer) { + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + if (!worker) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_WASM_ERROR_CONSUMING_RESPONSE); + return false; + } + + return FetchUtil::StreamResponseToJS(aCx, aObj, aMimeType, aConsumer, worker); +} + +bool InitJSContextForWorker(WorkerPrivate* aWorkerPrivate, + JSContext* aWorkerCx) { + aWorkerPrivate->AssertIsOnWorkerThread(); + NS_ASSERTION(!aWorkerPrivate->GetJSContext(), "Already has a context!"); + + JSSettings settings; + aWorkerPrivate->CopyJSSettings(settings); + + JS::ContextOptionsRef(aWorkerCx) = settings.contextOptions; + + // This is the real place where we set the max memory for the runtime. + for (const auto& setting : settings.gcSettings) { + if (setting.value) { + JS_SetGCParameter(aWorkerCx, setting.key, *setting.value); + } else { + JS_ResetGCParameter(aWorkerCx, setting.key); + } + } + + JS_SetNativeStackQuota(aWorkerCx, WORKER_CONTEXT_NATIVE_STACK_LIMIT); + + // Security policy: + static const JSSecurityCallbacks securityCallbacks = { + ContentSecurityPolicyAllows}; + JS_SetSecurityCallbacks(aWorkerCx, &securityCallbacks); + + // A WorkerPrivate lives strictly longer than its JSRuntime so we can safely + // store a raw pointer as the callback's closure argument on the JSRuntime. + JS::InitDispatchToEventLoop(aWorkerCx, DispatchToEventLoop, + (void*)aWorkerPrivate); + + JS::InitConsumeStreamCallback(aWorkerCx, ConsumeStream, + FetchUtil::ReportJSStreamError); + + // When available, set the self-hosted shared memory to be read, so that we + // can decode the self-hosted content instead of parsing it. + auto& shm = xpc::SelfHostedShmem::GetSingleton(); + JS::SelfHostedCache selfHostedContent = shm.Content(); + + if (!JS::InitSelfHostedCode(aWorkerCx, selfHostedContent)) { + NS_WARNING("Could not init self-hosted code!"); + return false; + } + + JS_AddInterruptCallback(aWorkerCx, InterruptCallback); + + JS::SetCTypesActivityCallback(aWorkerCx, CTypesActivityCallback); + +#ifdef JS_GC_ZEAL + JS_SetGCZeal(aWorkerCx, settings.gcZeal, settings.gcZealFrequency); +#endif + + return true; +} + +static bool PreserveWrapper(JSContext* cx, JS::Handle<JSObject*> obj) { + MOZ_ASSERT(cx); + MOZ_ASSERT(obj); + MOZ_ASSERT(mozilla::dom::IsDOMObject(obj)); + + return mozilla::dom::TryPreserveWrapper(obj); +} + +static bool IsWorkerDebuggerGlobalOrSandbox(JS::Handle<JSObject*> aGlobal) { + return IsWorkerDebuggerGlobal(aGlobal) || IsWorkerDebuggerSandbox(aGlobal); +} + +JSObject* Wrap(JSContext* cx, JS::Handle<JSObject*> existing, + JS::Handle<JSObject*> obj) { + JS::Rooted<JSObject*> targetGlobal(cx, JS::CurrentGlobalOrNull(cx)); + + // Note: the JS engine unwraps CCWs before calling this callback. + JS::Rooted<JSObject*> originGlobal(cx, JS::GetNonCCWObjectGlobal(obj)); + + const js::Wrapper* wrapper = nullptr; + if (IsWorkerDebuggerGlobalOrSandbox(targetGlobal) && + IsWorkerDebuggerGlobalOrSandbox(originGlobal)) { + wrapper = &js::CrossCompartmentWrapper::singleton; + } else { + wrapper = &js::OpaqueCrossCompartmentWrapper::singleton; + } + + if (existing) { + js::Wrapper::Renew(existing, obj, wrapper); + } + return js::Wrapper::New(cx, obj, wrapper); +} + +static const JSWrapObjectCallbacks WrapObjectCallbacks = { + Wrap, + nullptr, +}; + +class WorkerJSRuntime final : public mozilla::CycleCollectedJSRuntime { + public: + // The heap size passed here doesn't matter, we will change it later in the + // call to JS_SetGCParameter inside InitJSContextForWorker. + explicit WorkerJSRuntime(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + : CycleCollectedJSRuntime(aCx), mWorkerPrivate(aWorkerPrivate) { + MOZ_COUNT_CTOR_INHERITED(WorkerJSRuntime, CycleCollectedJSRuntime); + MOZ_ASSERT(aWorkerPrivate); + + { + JS::UniqueChars defaultLocale = aWorkerPrivate->AdoptDefaultLocale(); + MOZ_ASSERT(defaultLocale, + "failure of a WorkerPrivate to have a default locale should " + "have made the worker fail to spawn"); + + if (!JS_SetDefaultLocale(Runtime(), defaultLocale.get())) { + NS_WARNING("failed to set workerCx's default locale"); + } + } + } + + void Shutdown(JSContext* cx) override { + // The CC is shut down, and the superclass destructor will GC, so make sure + // we don't try to CC again. + mWorkerPrivate = nullptr; + + CycleCollectedJSRuntime::Shutdown(cx); + } + + ~WorkerJSRuntime() { + MOZ_COUNT_DTOR_INHERITED(WorkerJSRuntime, CycleCollectedJSRuntime); + } + + virtual void PrepareForForgetSkippable() override {} + + virtual void BeginCycleCollectionCallback( + mozilla::CCReason aReason) override {} + + virtual void EndCycleCollectionCallback( + CycleCollectorResults& aResults) override {} + + void DispatchDeferredDeletion(bool aContinuation, bool aPurge) override { + MOZ_ASSERT(!aContinuation); + + // Do it immediately, no need for asynchronous behavior here. + nsCycleCollector_doDeferredDeletion(); + } + + virtual void CustomGCCallback(JSGCStatus aStatus) override { + if (!mWorkerPrivate) { + // We're shutting down, no need to do anything. + return; + } + + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (aStatus == JSGC_END) { + bool collectedAnything = + nsCycleCollector_collect(CCReason::GC_FINISHED, nullptr); + mWorkerPrivate->SetCCCollectedAnything(collectedAnything); + } + } + + private: + WorkerPrivate* mWorkerPrivate; +}; + +} // anonymous namespace + +} // namespace workerinternals + +class WorkerJSContext final : public mozilla::CycleCollectedJSContext { + public: + // The heap size passed here doesn't matter, we will change it later in the + // call to JS_SetGCParameter inside InitJSContextForWorker. + explicit WorkerJSContext(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) { + MOZ_COUNT_CTOR_INHERITED(WorkerJSContext, CycleCollectedJSContext); + MOZ_ASSERT(aWorkerPrivate); + // Magical number 2. Workers have the base recursion depth 1, and normal + // runnables run at level 2, and we don't want to process microtasks + // at any other level. + SetTargetedMicroTaskRecursionDepth(2); + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY because otherwise we have to annotate the + // SpiderMonkey JS::JobQueue's destructor as MOZ_CAN_RUN_SCRIPT, which is a + // bit of a pain. + MOZ_CAN_RUN_SCRIPT_BOUNDARY ~WorkerJSContext() { + MOZ_COUNT_DTOR_INHERITED(WorkerJSContext, CycleCollectedJSContext); + JSContext* cx = MaybeContext(); + if (!cx) { + return; // Initialize() must have failed + } + + // We expect to come here with the cycle collector already shut down. + // The superclass destructor will run the GC one final time and finalize any + // JSObjects that were participating in cycles that were broken during CC + // shutdown. + // Make sure we don't try to CC again. + mWorkerPrivate = nullptr; + } + + WorkerJSContext* GetAsWorkerJSContext() override { return this; } + + CycleCollectedJSRuntime* CreateRuntime(JSContext* aCx) override { + return new WorkerJSRuntime(aCx, mWorkerPrivate); + } + + nsresult Initialize(JSRuntime* aParentRuntime) { + nsresult rv = CycleCollectedJSContext::Initialize( + aParentRuntime, WORKER_DEFAULT_RUNTIME_HEAPSIZE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JSContext* cx = Context(); + + js::SetPreserveWrapperCallbacks(cx, PreserveWrapper, HasReleasedWrapper); + JS_InitDestroyPrincipalsCallback(cx, nsJSPrincipals::Destroy); + JS_InitReadPrincipalsCallback(cx, nsJSPrincipals::ReadPrincipals); + JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); + if (mWorkerPrivate->IsDedicatedWorker()) { + JS_SetFutexCanWait(cx); + } + + return NS_OK; + } + + virtual void DispatchToMicroTask( + already_AddRefed<MicroTaskRunnable> aRunnable) override { + RefPtr<MicroTaskRunnable> runnable(aRunnable); + + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(runnable); + + std::deque<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr; + + JSContext* cx = Context(); + NS_ASSERTION(cx, "This should never be null!"); + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + NS_ASSERTION(global, "This should never be null!"); + + // On worker threads, if the current global is the worker global or + // ShadowRealm global, we use the main micro task queue. Otherwise, the + // current global must be either the debugger global or a debugger sandbox, + // and we use the debugger micro task queue instead. + if (IsWorkerGlobal(global) || IsShadowRealmGlobal(global)) { + microTaskQueue = &GetMicroTaskQueue(); + } else { + MOZ_ASSERT(IsWorkerDebuggerGlobal(global) || + IsWorkerDebuggerSandbox(global)); + + microTaskQueue = &GetDebuggerMicroTaskQueue(); + } + + JS::JobQueueMayNotBeEmpty(cx); + microTaskQueue->push_back(std::move(runnable)); + } + + bool IsSystemCaller() const override { + return mWorkerPrivate->UsesSystemPrincipal(); + } + + void ReportError(JSErrorReport* aReport, + JS::ConstUTF8CharsZ aToStringResult) override { + mWorkerPrivate->ReportError(Context(), aToStringResult, aReport); + } + + WorkerPrivate* GetWorkerPrivate() const { return mWorkerPrivate; } + + private: + WorkerPrivate* mWorkerPrivate; +}; + +namespace workerinternals { + +namespace { + +class WorkerThreadPrimaryRunnable final : public Runnable { + WorkerPrivate* mWorkerPrivate; + SafeRefPtr<WorkerThread> mThread; + JSRuntime* mParentRuntime; + + class FinishedRunnable final : public Runnable { + SafeRefPtr<WorkerThread> mThread; + + public: + explicit FinishedRunnable(SafeRefPtr<WorkerThread> aThread) + : Runnable("WorkerThreadPrimaryRunnable::FinishedRunnable"), + mThread(std::move(aThread)) { + MOZ_ASSERT(mThread); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(FinishedRunnable, Runnable) + + private: + ~FinishedRunnable() = default; + + NS_DECL_NSIRUNNABLE + }; + + public: + WorkerThreadPrimaryRunnable(WorkerPrivate* aWorkerPrivate, + SafeRefPtr<WorkerThread> aThread, + JSRuntime* aParentRuntime) + : mozilla::Runnable("WorkerThreadPrimaryRunnable"), + mWorkerPrivate(aWorkerPrivate), + mThread(std::move(aThread)), + mParentRuntime(aParentRuntime) { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(mThread); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(WorkerThreadPrimaryRunnable, Runnable) + + private: + ~WorkerThreadPrimaryRunnable() = default; + + NS_DECL_NSIRUNNABLE +}; + +void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) { + AssertIsOnMainThread(); + + nsTArray<nsString> languages; + Navigator::GetAcceptLanguages(languages); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAllWorkerLanguages(languages); + } +} + +void AppVersionOverrideChanged(const char* /* aPrefName */, + void* /* aClosure */) { + AssertIsOnMainThread(); + + nsAutoString override; + Preferences::GetString("general.appversion.override", override); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAppVersionOverridePreference(override); + } +} + +void PlatformOverrideChanged(const char* /* aPrefName */, + void* /* aClosure */) { + AssertIsOnMainThread(); + + nsAutoString override; + Preferences::GetString("general.platform.override", override); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdatePlatformOverridePreference(override); + } +} + +} /* anonymous namespace */ + +// This is only touched on the main thread. Initialized in Init() below. +StaticAutoPtr<JSSettings> RuntimeService::sDefaultJSSettings; + +RuntimeService::RuntimeService() + : mMutex("RuntimeService::mMutex"), + mObserved(false), + mShuttingDown(false), + mNavigatorPropertiesLoaded(false) { + AssertIsOnMainThread(); + MOZ_ASSERT(!GetService(), "More than one service!"); +} + +RuntimeService::~RuntimeService() { + AssertIsOnMainThread(); + + // gRuntimeService can be null if Init() fails. + MOZ_ASSERT(!GetService() || GetService() == this, "More than one service!"); + + gRuntimeService = nullptr; +} + +// static +RuntimeService* RuntimeService::GetOrCreateService() { + AssertIsOnMainThread(); + + if (!gRuntimeService) { + // The observer service now owns us until shutdown. + gRuntimeService = new RuntimeService(); + if (NS_FAILED((*gRuntimeService).Init())) { + NS_WARNING("Failed to initialize!"); + (*gRuntimeService).Cleanup(); + gRuntimeService = nullptr; + return nullptr; + } + } + + return gRuntimeService; +} + +// static +RuntimeService* RuntimeService::GetService() { return gRuntimeService; } + +bool RuntimeService::RegisterWorker(WorkerPrivate& aWorkerPrivate) { + aWorkerPrivate.AssertIsOnParentThread(); + + WorkerPrivate* parent = aWorkerPrivate.GetParent(); + if (!parent) { + AssertIsOnMainThread(); + + if (mShuttingDown) { + return false; + } + } + + const bool isServiceWorker = aWorkerPrivate.IsServiceWorker(); + const bool isSharedWorker = aWorkerPrivate.IsSharedWorker(); + const bool isDedicatedWorker = aWorkerPrivate.IsDedicatedWorker(); + if (isServiceWorker) { + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_SPAWN_ATTEMPTS, 1); + } + + nsCString sharedWorkerScriptSpec; + if (isSharedWorker) { + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> scriptURI = aWorkerPrivate.GetResolvedScriptURI(); + NS_ASSERTION(scriptURI, "Null script URI!"); + + nsresult rv = scriptURI->GetSpec(sharedWorkerScriptSpec); + if (NS_FAILED(rv)) { + NS_WARNING("GetSpec failed?!"); + return false; + } + + NS_ASSERTION(!sharedWorkerScriptSpec.IsEmpty(), "Empty spec!"); + } + + bool exemptFromPerDomainMax = false; + if (isServiceWorker) { + AssertIsOnMainThread(); + exemptFromPerDomainMax = Preferences::GetBool( + "dom.serviceWorkers.exemptFromPerDomainMax", false); + } + + const nsCString& domain = aWorkerPrivate.Domain(); + + bool queued = false; + { + MutexAutoLock lock(mMutex); + + auto* const domainInfo = + mDomainMap + .LookupOrInsertWith( + domain, + [&domain, parent] { + NS_ASSERTION(!parent, "Shouldn't have a parent here!"); + Unused << parent; // silence clang -Wunused-lambda-capture in + // opt builds + auto wdi = MakeUnique<WorkerDomainInfo>(); + wdi->mDomain = domain; + return wdi; + }) + .get(); + + queued = gMaxWorkersPerDomain && + domainInfo->ActiveWorkerCount() >= gMaxWorkersPerDomain && + !domain.IsEmpty() && !exemptFromPerDomainMax; + + if (queued) { + domainInfo->mQueuedWorkers.AppendElement(&aWorkerPrivate); + + // Worker spawn gets queued due to hitting max workers per domain + // limit so let's log a warning. + WorkerPrivate::ReportErrorToConsole("HittingMaxWorkersPerDomain2"); + + if (isServiceWorker) { + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_SPAWN_GETS_QUEUED, 1); + } else if (isSharedWorker) { + Telemetry::Accumulate(Telemetry::SHARED_WORKER_SPAWN_GETS_QUEUED, 1); + } else if (isDedicatedWorker) { + Telemetry::Accumulate(Telemetry::DEDICATED_WORKER_SPAWN_GETS_QUEUED, 1); + } + } else if (parent) { + domainInfo->mChildWorkerCount++; + } else if (isServiceWorker) { + domainInfo->mActiveServiceWorkers.AppendElement(&aWorkerPrivate); + } else { + domainInfo->mActiveWorkers.AppendElement(&aWorkerPrivate); + } + } + + // From here on out we must call UnregisterWorker if something fails! + if (parent) { + if (!parent->AddChildWorker(aWorkerPrivate)) { + UnregisterWorker(aWorkerPrivate); + return false; + } + } else { + if (!mNavigatorPropertiesLoaded) { + if (NS_FAILED(Navigator::GetAppVersion( + mNavigatorProperties.mAppVersion, aWorkerPrivate.GetDocument(), + false /* aUsePrefOverriddenValue */)) || + NS_FAILED(Navigator::GetPlatform( + mNavigatorProperties.mPlatform, aWorkerPrivate.GetDocument(), + false /* aUsePrefOverriddenValue */))) { + UnregisterWorker(aWorkerPrivate); + return false; + } + + // The navigator overridden properties should have already been read. + + Navigator::GetAcceptLanguages(mNavigatorProperties.mLanguages); + mNavigatorPropertiesLoaded = true; + } + + nsPIDOMWindowInner* window = aWorkerPrivate.GetWindow(); + + if (!isServiceWorker) { + // Service workers are excluded since their lifetime is separate from + // that of dom windows. + if (auto* const windowArray = mWindowMap.GetOrInsertNew(window, 1); + !windowArray->Contains(&aWorkerPrivate)) { + windowArray->AppendElement(&aWorkerPrivate); + } else { + MOZ_ASSERT(aWorkerPrivate.IsSharedWorker()); + } + } + } + + if (!queued && !ScheduleWorker(aWorkerPrivate)) { + return false; + } + + if (isServiceWorker) { + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_WAS_SPAWNED, 1); + } + return true; +} + +void RuntimeService::UnregisterWorker(WorkerPrivate& aWorkerPrivate) { + aWorkerPrivate.AssertIsOnParentThread(); + + WorkerPrivate* parent = aWorkerPrivate.GetParent(); + if (!parent) { + AssertIsOnMainThread(); + } + + const nsCString& domain = aWorkerPrivate.Domain(); + + WorkerPrivate* queuedWorker = nullptr; + { + MutexAutoLock lock(mMutex); + + WorkerDomainInfo* domainInfo; + if (!mDomainMap.Get(domain, &domainInfo)) { + NS_ERROR("Don't have an entry for this domain!"); + } + + // Remove old worker from everywhere. + uint32_t index = domainInfo->mQueuedWorkers.IndexOf(&aWorkerPrivate); + if (index != kNoIndex) { + // Was queued, remove from the list. + domainInfo->mQueuedWorkers.RemoveElementAt(index); + } else if (parent) { + MOZ_ASSERT(domainInfo->mChildWorkerCount, "Must be non-zero!"); + domainInfo->mChildWorkerCount--; + } else if (aWorkerPrivate.IsServiceWorker()) { + MOZ_ASSERT(domainInfo->mActiveServiceWorkers.Contains(&aWorkerPrivate), + "Don't know about this worker!"); + domainInfo->mActiveServiceWorkers.RemoveElement(&aWorkerPrivate); + } else { + MOZ_ASSERT(domainInfo->mActiveWorkers.Contains(&aWorkerPrivate), + "Don't know about this worker!"); + domainInfo->mActiveWorkers.RemoveElement(&aWorkerPrivate); + } + + // See if there's a queued worker we can schedule. + if (domainInfo->ActiveWorkerCount() < gMaxWorkersPerDomain && + !domainInfo->mQueuedWorkers.IsEmpty()) { + queuedWorker = domainInfo->mQueuedWorkers[0]; + domainInfo->mQueuedWorkers.RemoveElementAt(0); + + if (queuedWorker->GetParent()) { + domainInfo->mChildWorkerCount++; + } else if (queuedWorker->IsServiceWorker()) { + domainInfo->mActiveServiceWorkers.AppendElement(queuedWorker); + } else { + domainInfo->mActiveWorkers.AppendElement(queuedWorker); + } + } + + if (domainInfo->HasNoWorkers()) { + MOZ_ASSERT(domainInfo->mQueuedWorkers.IsEmpty()); + mDomainMap.Remove(domain); + } + } + + // NB: For Shared Workers we used to call ShutdownOnMainThread on the + // RemoteWorkerController; however, that was redundant because + // RemoteWorkerChild uses a WeakWorkerRef which notifies at about the + // same time as us calling into the code here and would race with us. + + if (parent) { + parent->RemoveChildWorker(aWorkerPrivate); + } else if (aWorkerPrivate.IsSharedWorker()) { + AssertIsOnMainThread(); + + mWindowMap.RemoveIf([&aWorkerPrivate](const auto& iter) { + const auto& workers = iter.Data(); + MOZ_ASSERT(workers); + + if (workers->RemoveElement(&aWorkerPrivate)) { + MOZ_ASSERT(!workers->Contains(&aWorkerPrivate), + "Added worker more than once!"); + + return workers->IsEmpty(); + } + + return false; + }); + } else if (aWorkerPrivate.IsDedicatedWorker()) { + // May be null. + nsPIDOMWindowInner* window = aWorkerPrivate.GetWindow(); + if (auto entry = mWindowMap.Lookup(window)) { + MOZ_ALWAYS_TRUE(entry.Data()->RemoveElement(&aWorkerPrivate)); + if (entry.Data()->IsEmpty()) { + entry.Remove(); + } + } else { + MOZ_ASSERT_UNREACHABLE("window is not in mWindowMap"); + } + } + + if (queuedWorker && !ScheduleWorker(*queuedWorker)) { + UnregisterWorker(*queuedWorker); + } +} + +bool RuntimeService::ScheduleWorker(WorkerPrivate& aWorkerPrivate) { + if (!aWorkerPrivate.Start()) { + // This is ok, means that we didn't need to make a thread for this worker. + return true; + } + + const WorkerThreadFriendKey friendKey; + + SafeRefPtr<WorkerThread> thread = WorkerThread::Create(friendKey); + if (!thread) { + UnregisterWorker(aWorkerPrivate); + return false; + } + + if (NS_FAILED(thread->SetPriority(nsISupportsPriority::PRIORITY_NORMAL))) { + NS_WARNING("Could not set the thread's priority!"); + } + + aWorkerPrivate.SetThread(thread.unsafeGetRawPtr()); + JSContext* cx = CycleCollectedJSContext::Get()->Context(); + nsCOMPtr<nsIRunnable> runnable = new WorkerThreadPrimaryRunnable( + &aWorkerPrivate, thread.clonePtr(), JS_GetParentRuntime(cx)); + if (NS_FAILED( + thread->DispatchPrimaryRunnable(friendKey, runnable.forget()))) { + UnregisterWorker(aWorkerPrivate); + return false; + } + + return true; +} + +nsresult RuntimeService::Init() { + AssertIsOnMainThread(); + + nsLayoutStatics::AddRef(); + + // Initialize JSSettings. + sDefaultJSSettings = new JSSettings(); + SetDefaultJSGCSettings(JSGC_MAX_BYTES, Some(WORKER_DEFAULT_RUNTIME_HEAPSIZE)); + SetDefaultJSGCSettings(JSGC_ALLOCATION_THRESHOLD, + Some(WORKER_DEFAULT_ALLOCATION_THRESHOLD)); + + // nsIStreamTransportService is thread-safe but it must be initialized on the + // main-thread. FileReader needs it, so, let's initialize it now. + nsresult rv; + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + NS_ENSURE_TRUE(sts, NS_ERROR_FAILURE); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + mObserved = true; + + if (NS_FAILED(obs->AddObserver(this, GC_REQUEST_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for GC request notifications!"); + } + + if (NS_FAILED(obs->AddObserver(this, CC_REQUEST_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for CC request notifications!"); + } + + if (NS_FAILED( + obs->AddObserver(this, MEMORY_PRESSURE_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for memory pressure notifications!"); + } + + if (NS_FAILED( + obs->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false))) { + NS_WARNING("Failed to register for offline notification event!"); + } + + MOZ_ASSERT(!gRuntimeServiceDuringInit, "This should be false!"); + gRuntimeServiceDuringInit = true; + +#define WORKER_PREF(name, callback) \ + NS_FAILED(Preferences::RegisterCallbackAndCall(callback, name)) + + if (NS_FAILED(Preferences::RegisterPrefixCallbackAndCall( + LoadJSGCMemoryOptions, + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX)) || +#ifdef JS_GC_ZEAL + NS_FAILED(Preferences::RegisterCallback( + LoadGCZealOptions, PREF_JS_OPTIONS_PREFIX PREF_GCZEAL)) || +#endif + WORKER_PREF("intl.accept_languages", PrefLanguagesChanged) || + WORKER_PREF("general.appversion.override", AppVersionOverrideChanged) || + WORKER_PREF("general.platform.override", PlatformOverrideChanged) || + NS_FAILED(Preferences::RegisterPrefixCallbackAndCall( + LoadContextOptions, PREF_JS_OPTIONS_PREFIX))) { + NS_WARNING("Failed to register pref callbacks!"); + } + +#undef WORKER_PREF + + MOZ_ASSERT(gRuntimeServiceDuringInit, "Should be true!"); + gRuntimeServiceDuringInit = false; + + int32_t maxPerDomain = + Preferences::GetInt(PREF_WORKERS_MAX_PER_DOMAIN, MAX_WORKERS_PER_DOMAIN); + gMaxWorkersPerDomain = std::max(0, maxPerDomain); + + if (NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate())) { + return NS_ERROR_UNEXPECTED; + } + + // PerformanceService must be initialized on the main-thread. + PerformanceService::GetOrCreate(); + + return NS_OK; +} + +void RuntimeService::Shutdown() { + AssertIsOnMainThread(); + + MOZ_ASSERT(!mShuttingDown); + // That's it, no more workers. + mShuttingDown = true; + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Failed to get observer service?!"); + + // Tell anyone that cares that they're about to lose worker support. + if (obs && NS_FAILED(obs->NotifyObservers(nullptr, WORKERS_SHUTDOWN_TOPIC, + nullptr))) { + NS_WARNING("NotifyObservers failed!"); + } + + { + AutoTArray<WorkerPrivate*, 100> workers; + + { + MutexAutoLock lock(mMutex); + + AddAllTopLevelWorkersToArray(workers); + } + + // Cancel all top-level workers. + for (const auto& worker : workers) { + if (!worker->Cancel()) { + NS_WARNING("Failed to cancel worker!"); + } + } + } + + sDefaultJSSettings = nullptr; +} + +namespace { + +class DumpCrashInfoRunnable final : public WorkerControlRunnable { + public: + explicit DumpCrashInfoRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, "DumpCrashInfoRunnable", + WorkerThread), + mMonitor("DumpCrashInfoRunnable::mMonitor") {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MonitorAutoLock lock(mMonitor); + if (!mHasMsg) { + aWorkerPrivate->DumpCrashInformation(mMsg); + mHasMsg.Flip(); + } + lock.Notify(); + return true; + } + + nsresult Cancel() override { + MonitorAutoLock lock(mMonitor); + if (!mHasMsg) { + mMsg.Assign("Canceled"); + mHasMsg.Flip(); + } + lock.Notify(); + + return NS_OK; + } + + bool DispatchAndWait() { + MonitorAutoLock lock(mMonitor); + + if (!Dispatch()) { + // The worker is already dead but the main thread still didn't remove it + // from RuntimeService's registry. + return false; + } + + // To avoid any possibility of process hangs we never receive reports on + // we give the worker 1sec to react. + lock.Wait(TimeDuration::FromMilliseconds(1000)); + if (!mHasMsg) { + mMsg.Append("NoResponse"); + mHasMsg.Flip(); + } + return true; + } + + const nsCString& MsgData() const { return mMsg; } + + private: + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override {} + + Monitor mMonitor MOZ_UNANNOTATED; + nsCString mMsg; + FlippedOnce<false> mHasMsg; +}; + +struct ActiveWorkerStats { + template <uint32_t ActiveWorkerStats::*Category> + void Update(const nsTArray<WorkerPrivate*>& aWorkers) { + for (const auto worker : aWorkers) { + RefPtr<DumpCrashInfoRunnable> runnable = + new DumpCrashInfoRunnable(worker); + if (runnable->DispatchAndWait()) { + ++(this->*Category); + mMessage.Append(runnable->MsgData()); + } + } + } + + uint32_t mWorkers = 0; + uint32_t mServiceWorkers = 0; + nsCString mMessage; +}; + +} // namespace + +void RuntimeService::CrashIfHanging() { + MutexAutoLock lock(mMutex); + + // If we never wanted to shut down we cannot hang. + if (!mShuttingDown) { + return; + } + + ActiveWorkerStats activeStats; + uint32_t inactiveWorkers = 0; + + for (const auto& aData : mDomainMap.Values()) { + activeStats.Update<&ActiveWorkerStats::mWorkers>(aData->mActiveWorkers); + activeStats.Update<&ActiveWorkerStats::mServiceWorkers>( + aData->mActiveServiceWorkers); + + // These might not be top-level workers... + inactiveWorkers += std::count_if( + aData->mQueuedWorkers.begin(), aData->mQueuedWorkers.end(), + [](const auto* const worker) { return !worker->GetParent(); }); + } + + if (activeStats.mWorkers + activeStats.mServiceWorkers + inactiveWorkers == + 0) { + return; + } + + nsCString msg; + + // A: active Workers | S: active ServiceWorkers | Q: queued Workers + msg.AppendPrintf("Workers Hanging - %d|A:%d|S:%d|Q:%d", mShuttingDown ? 1 : 0, + activeStats.mWorkers, activeStats.mServiceWorkers, + inactiveWorkers); + msg.Append(activeStats.mMessage); + + // This string will be leaked. + MOZ_CRASH_UNSAFE(strdup(msg.BeginReading())); +} + +// This spins the event loop until all workers are finished and their threads +// have been joined. +void RuntimeService::Cleanup() { + AssertIsOnMainThread(); + + if (!mShuttingDown) { + Shutdown(); + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Failed to get observer service?!"); + + { + MutexAutoLock lock(mMutex); + + AutoTArray<WorkerPrivate*, 100> workers; + AddAllTopLevelWorkersToArray(workers); + + if (!workers.IsEmpty()) { + nsIThread* currentThread = NS_GetCurrentThread(); + NS_ASSERTION(currentThread, "This should never be null!"); + + // If the loop below takes too long, we probably have a problematic + // worker. MOZ_LOG some info before the parent process forcibly + // terminates us so that in the event we are a content process, the log + // output can provide useful context about the workers that did not + // cleanly shut down. + nsCOMPtr<nsITimer> timer; + RefPtr<RuntimeService> self = this; + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(timer), + [self](nsITimer*) { self->DumpRunningWorkers(); }, + TimeDuration::FromSeconds(1), nsITimer::TYPE_ONE_SHOT, + "RuntimeService::WorkerShutdownDump"); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + // And make sure all their final messages have run and all their threads + // have joined. + while (mDomainMap.Count()) { + MutexAutoUnlock unlock(mMutex); + + if (!NS_ProcessNextEvent(currentThread)) { + NS_WARNING("Something bad happened!"); + break; + } + } + + if (NS_SUCCEEDED(rv)) { + timer->Cancel(); + } + } + } + + NS_ASSERTION(!mWindowMap.Count(), "All windows should have been released!"); + +#define WORKER_PREF(name, callback) \ + NS_FAILED(Preferences::UnregisterCallback(callback, name)) + + if (mObserved) { + if (NS_FAILED(Preferences::UnregisterPrefixCallback( + LoadContextOptions, PREF_JS_OPTIONS_PREFIX)) || + WORKER_PREF("intl.accept_languages", PrefLanguagesChanged) || + WORKER_PREF("general.appversion.override", AppVersionOverrideChanged) || + WORKER_PREF("general.platform.override", PlatformOverrideChanged) || +#ifdef JS_GC_ZEAL + NS_FAILED(Preferences::UnregisterCallback( + LoadGCZealOptions, PREF_JS_OPTIONS_PREFIX PREF_GCZEAL)) || +#endif + NS_FAILED(Preferences::UnregisterPrefixCallback( + LoadJSGCMemoryOptions, + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX))) { + NS_WARNING("Failed to unregister pref callbacks!"); + } + +#undef WORKER_PREF + + if (obs) { + if (NS_FAILED(obs->RemoveObserver(this, GC_REQUEST_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for GC request notifications!"); + } + + if (NS_FAILED(obs->RemoveObserver(this, CC_REQUEST_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for CC request notifications!"); + } + + if (NS_FAILED( + obs->RemoveObserver(this, MEMORY_PRESSURE_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for memory pressure notifications!"); + } + + if (NS_FAILED( + obs->RemoveObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC))) { + NS_WARNING("Failed to unregister for offline notification event!"); + } + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + mObserved = false; + } + } + + nsLayoutStatics::Release(); +} + +void RuntimeService::AddAllTopLevelWorkersToArray( + nsTArray<WorkerPrivate*>& aWorkers) { + for (const auto& aData : mDomainMap.Values()) { +#ifdef DEBUG + for (const auto& activeWorker : aData->mActiveWorkers) { + MOZ_ASSERT(!activeWorker->GetParent(), + "Shouldn't have a parent in this list!"); + } + for (const auto& activeServiceWorker : aData->mActiveServiceWorkers) { + MOZ_ASSERT(!activeServiceWorker->GetParent(), + "Shouldn't have a parent in this list!"); + } +#endif + + aWorkers.AppendElements(aData->mActiveWorkers); + aWorkers.AppendElements(aData->mActiveServiceWorkers); + + // These might not be top-level workers... + std::copy_if(aData->mQueuedWorkers.begin(), aData->mQueuedWorkers.end(), + MakeBackInserter(aWorkers), + [](const auto& worker) { return !worker->GetParent(); }); + } +} + +nsTArray<WorkerPrivate*> RuntimeService::GetWorkersForWindow( + const nsPIDOMWindowInner& aWindow) const { + AssertIsOnMainThread(); + + nsTArray<WorkerPrivate*> result; + if (nsTArray<WorkerPrivate*>* const workers = mWindowMap.Get(&aWindow)) { + NS_ASSERTION(!workers->IsEmpty(), "Should have been removed!"); + result.AppendElements(*workers); + } + return result; +} + +void RuntimeService::CancelWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->Cancel(); + } +} + +void RuntimeService::FreezeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->Freeze(&aWindow); + } +} + +void RuntimeService::ThawWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->Thaw(&aWindow); + } +} + +void RuntimeService::SuspendWorkersForWindow( + const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->ParentWindowPaused(); + } +} + +void RuntimeService::ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + MOZ_ASSERT(!worker->IsSharedWorker()); + worker->ParentWindowResumed(); + } +} + +void RuntimeService::PropagateStorageAccessPermissionGranted( + const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + MOZ_ASSERT_IF(aWindow.GetExtantDoc(), aWindow.GetExtantDoc() + ->CookieJarSettings() + ->GetRejectThirdPartyContexts()); + + for (WorkerPrivate* const worker : GetWorkersForWindow(aWindow)) { + worker->PropagateStorageAccessPermissionGranted(); + } +} + +template <typename Func> +void RuntimeService::BroadcastAllWorkers(const Func& aFunc) { + AssertIsOnMainThread(); + + AutoTArray<WorkerPrivate*, 100> workers; + { + MutexAutoLock lock(mMutex); + + AddAllTopLevelWorkersToArray(workers); + } + + for (const auto& worker : workers) { + aFunc(*worker); + } +} + +void RuntimeService::UpdateAllWorkerContextOptions() { + BroadcastAllWorkers([](auto& worker) { + worker.UpdateContextOptions(sDefaultJSSettings->contextOptions); + }); +} + +void RuntimeService::UpdateAppVersionOverridePreference( + const nsAString& aValue) { + AssertIsOnMainThread(); + mNavigatorProperties.mAppVersionOverridden = aValue; +} + +void RuntimeService::UpdatePlatformOverridePreference(const nsAString& aValue) { + AssertIsOnMainThread(); + mNavigatorProperties.mPlatformOverridden = aValue; +} + +void RuntimeService::UpdateAllWorkerLanguages( + const nsTArray<nsString>& aLanguages) { + MOZ_ASSERT(NS_IsMainThread()); + + mNavigatorProperties.mLanguages = aLanguages.Clone(); + BroadcastAllWorkers( + [&aLanguages](auto& worker) { worker.UpdateLanguages(aLanguages); }); +} + +void RuntimeService::UpdateAllWorkerMemoryParameter(JSGCParamKey aKey, + Maybe<uint32_t> aValue) { + BroadcastAllWorkers([aKey, aValue](auto& worker) { + worker.UpdateJSWorkerMemoryParameter(aKey, aValue); + }); +} + +#ifdef JS_GC_ZEAL +void RuntimeService::UpdateAllWorkerGCZeal() { + BroadcastAllWorkers([](auto& worker) { + worker.UpdateGCZeal(sDefaultJSSettings->gcZeal, + sDefaultJSSettings->gcZealFrequency); + }); +} +#endif + +void RuntimeService::SetLowMemoryStateAllWorkers(bool aState) { + BroadcastAllWorkers( + [aState](auto& worker) { worker.SetLowMemoryState(aState); }); +} + +void RuntimeService::GarbageCollectAllWorkers(bool aShrinking) { + BroadcastAllWorkers( + [aShrinking](auto& worker) { worker.GarbageCollect(aShrinking); }); +} + +void RuntimeService::CycleCollectAllWorkers() { + BroadcastAllWorkers([](auto& worker) { worker.CycleCollect(); }); +} + +void RuntimeService::SendOfflineStatusChangeEventToAllWorkers(bool aIsOffline) { + BroadcastAllWorkers([aIsOffline](auto& worker) { + worker.OfflineStatusChangeEvent(aIsOffline); + }); +} + +void RuntimeService::MemoryPressureAllWorkers() { + BroadcastAllWorkers([](auto& worker) { worker.MemoryPressure(); }); +} + +uint32_t RuntimeService::ClampedHardwareConcurrency( + bool aShouldResistFingerprinting) const { + // The Firefox Hardware Report says 70% of Firefox users have exactly 2 cores. + // When the resistFingerprinting pref is set, we want to blend into the crowd + // so spoof navigator.hardwareConcurrency = 2 to reduce user uniqueness. + if (MOZ_UNLIKELY(aShouldResistFingerprinting)) { + return 2; + } + + // This needs to be atomic, because multiple workers, and even mainthread, + // could race to initialize it at once. + static Atomic<uint32_t> unclampedHardwareConcurrency; + + // No need to loop here: if compareExchange fails, that just means that some + // other worker has initialized numberOfProcessors, so we're good to go. + if (!unclampedHardwareConcurrency) { + int32_t numberOfProcessors = 0; +#if defined(XP_MACOSX) + if (nsMacUtilsImpl::IsTCSMAvailable()) { + // On failure, zero is returned from GetPhysicalCPUCount() + // and we fallback to PR_GetNumberOfProcessors below. + numberOfProcessors = nsMacUtilsImpl::GetPhysicalCPUCount(); + } +#endif + if (numberOfProcessors == 0) { + numberOfProcessors = PR_GetNumberOfProcessors(); + } + if (numberOfProcessors <= 0) { + numberOfProcessors = 1; // Must be one there somewhere + } + Unused << unclampedHardwareConcurrency.compareExchange(0, + numberOfProcessors); + } + + return std::min(uint32_t(unclampedHardwareConcurrency), + StaticPrefs::dom_maxHardwareConcurrency()); +} + +// nsISupports +NS_IMPL_ISUPPORTS(RuntimeService, nsIObserver) + +// nsIObserver +NS_IMETHODIMP +RuntimeService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + AssertIsOnMainThread(); + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Shutdown(); + return NS_OK; + } + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID)) { + Cleanup(); + return NS_OK; + } + if (!strcmp(aTopic, GC_REQUEST_OBSERVER_TOPIC)) { + GarbageCollectAllWorkers(/* shrinking = */ false); + return NS_OK; + } + if (!strcmp(aTopic, CC_REQUEST_OBSERVER_TOPIC)) { + CycleCollectAllWorkers(); + return NS_OK; + } + if (!strcmp(aTopic, MEMORY_PRESSURE_OBSERVER_TOPIC)) { + nsDependentString data(aData); + // Don't continue to GC/CC if we are in an ongoing low-memory state since + // its very slow and it likely won't help us anyway. + if (data.EqualsLiteral(LOW_MEMORY_ONGOING_DATA)) { + return NS_OK; + } + if (data.EqualsLiteral(LOW_MEMORY_DATA)) { + SetLowMemoryStateAllWorkers(true); + } + GarbageCollectAllWorkers(/* shrinking = */ true); + CycleCollectAllWorkers(); + MemoryPressureAllWorkers(); + return NS_OK; + } + if (!strcmp(aTopic, MEMORY_PRESSURE_STOP_OBSERVER_TOPIC)) { + SetLowMemoryStateAllWorkers(false); + return NS_OK; + } + if (!strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC)) { + SendOfflineStatusChangeEventToAllWorkers(NS_IsOffline()); + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("Unknown observer topic!"); + return NS_OK; +} + +namespace { +const char* WorkerKindToString(WorkerKind kind) { + switch (kind) { + case WorkerKindDedicated: + return "dedicated"; + case WorkerKindShared: + return "shared"; + case WorkerKindService: + return "service"; + default: + NS_WARNING("Unknown worker type"); + return "unknown worker type"; + } +} + +void LogWorker(WorkerPrivate* worker, const char* category) { + AssertIsOnMainThread(); + + SHUTDOWN_LOG(("Found %s (%s): %s", category, + WorkerKindToString(worker->Kind()), + NS_ConvertUTF16toUTF8(worker->ScriptURL()).get())); + + if (worker->Kind() == WorkerKindService) { + SHUTDOWN_LOG(("Scope: %s", worker->ServiceWorkerScope().get())); + } + + nsCString origin; + worker->GetPrincipal()->GetOrigin(origin); + SHUTDOWN_LOG(("Principal: %s", origin.get())); + + nsCString loadingOrigin; + worker->GetLoadingPrincipal()->GetOrigin(loadingOrigin); + SHUTDOWN_LOG(("LoadingPrincipal: %s", loadingOrigin.get())); + + RefPtr<DumpCrashInfoRunnable> runnable = new DumpCrashInfoRunnable(worker); + if (runnable->DispatchAndWait()) { + SHUTDOWN_LOG(("CrashInfo: %s", runnable->MsgData().get())); + } else { + SHUTDOWN_LOG(("CrashInfo: dispatch failed")); + } +} +} // namespace + +void RuntimeService::DumpRunningWorkers() { + // Temporarily set the LogLevel high enough to be certain the messages are + // visible. + LogModule* module = gWorkerShutdownDumpLog; + LogLevel prevLevel = module->Level(); + + const auto cleanup = + MakeScopeExit([module, prevLevel] { module->SetLevel(prevLevel); }); + + if (prevLevel < LogLevel::Debug) { + module->SetLevel(LogLevel::Debug); + } + + MutexAutoLock lock(mMutex); + + for (const auto& info : mDomainMap.Values()) { + for (WorkerPrivate* worker : info->mActiveWorkers) { + LogWorker(worker, "ActiveWorker"); + } + + for (WorkerPrivate* worker : info->mActiveServiceWorkers) { + LogWorker(worker, "ActiveServiceWorker"); + } + + for (WorkerPrivate* worker : info->mQueuedWorkers) { + LogWorker(worker, "QueuedWorker"); + } + } +} + +bool LogViolationDetailsRunnable::MainThreadRun() { + AssertIsOnMainThread(); + + nsIContentSecurityPolicy* csp = mWorkerPrivate->GetCsp(); + if (csp) { + csp->LogViolationDetails(mViolationType, + nullptr, // triggering element + mWorkerPrivate->CSPEventListener(), mFileName, + mScriptSample, mLineNum, mColumnNum, u""_ns, + u""_ns); + } + + return true; +} + +// MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See +// bug 1535398. +MOZ_CAN_RUN_SCRIPT_BOUNDARY +NS_IMETHODIMP +WorkerThreadPrimaryRunnable::Run() { + NS_ConvertUTF16toUTF8 url(mWorkerPrivate->ScriptURL()); + AUTO_PROFILER_LABEL_DYNAMIC_CSTR("WorkerThreadPrimaryRunnable::Run", OTHER, + url.get()); + + using mozilla::ipc::BackgroundChild; + + { + auto failureCleanup = MakeScopeExit([&]() { + // The creation of threadHelper above is the point at which a worker is + // considered to have run, because the `mPreStartRunnables` are all + // re-dispatched after `mThread` is set. We need to let the WorkerPrivate + // know so it can clean up the various event loops and delete the worker. + mWorkerPrivate->RunLoopNeverRan(); + }); + + mWorkerPrivate->SetWorkerPrivateInWorkerThread(mThread.unsafeGetRawPtr()); + + const auto threadCleanup = MakeScopeExit([&] { + // This must be called before ScheduleDeletion, which is either called + // from failureCleanup leaving scope, or from the outer scope. + mWorkerPrivate->ResetWorkerPrivateInWorkerThread(); + }); + + mWorkerPrivate->AssertIsOnWorkerThread(); + + // This needs to be initialized on the worker thread before being used on + // the main thread and calling BackgroundChild::GetOrCreateForCurrentThread + // exposes it to the main thread. + mWorkerPrivate->EnsurePerformanceStorage(); + + if (NS_WARN_IF(!BackgroundChild::GetOrCreateForCurrentThread())) { + return NS_ERROR_FAILURE; + } + + nsWeakPtr globalScopeSentinel; + nsWeakPtr debuggerScopeSentinel; + // Never use the following pointers without checking their corresponding + // nsWeakPtr sentinel, defined above and initialized after DoRunLoop ends. + WorkerGlobalScopeBase* globalScopeRawPtr = nullptr; + WorkerGlobalScopeBase* debuggerScopeRawPtr = nullptr; + { + nsCycleCollector_startup(); + + auto context = MakeUnique<WorkerJSContext>(mWorkerPrivate); + nsresult rv = context->Initialize(mParentRuntime); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JSContext* cx = context->Context(); + + if (!InitJSContextForWorker(mWorkerPrivate, cx)) { + return NS_ERROR_FAILURE; + } + + failureCleanup.release(); + + { + PROFILER_SET_JS_CONTEXT(cx); + + { + // We're on the worker thread here, and WorkerPrivate's refcounting is + // non-threadsafe: you can only do it on the parent thread. What that + // means in practice is that we're relying on it being kept alive + // while we run. Hopefully. + MOZ_KnownLive(mWorkerPrivate)->DoRunLoop(cx); + // The AutoJSAPI in DoRunLoop should have reported any exceptions left + // on cx. + MOZ_ASSERT(!JS_IsExceptionPending(cx)); + } + + mWorkerPrivate->ShutdownModuleLoader(); + + mWorkerPrivate->RunShutdownTasks(); + + BackgroundChild::CloseForCurrentThread(); + + PROFILER_CLEAR_JS_CONTEXT(); + } + + // There may still be runnables on the debugger event queue that hold a + // strong reference to the debugger global scope. These runnables are not + // visible to the cycle collector, so we need to make sure to clear the + // debugger event queue before we try to destroy the context. If we don't, + // the garbage collector will crash. + // Note that this just releases the runnables and does not execute them. + mWorkerPrivate->ClearDebuggerEventQueue(); + + // Before shutting down the cycle collector we need to do one more pass + // through the event loop to clean up any C++ objects that need deferred + // cleanup. + NS_ProcessPendingEvents(nullptr); + + // At this point we expect the scopes to be alive if they were ever + // created successfully, keep weak references and set up the sentinels. + globalScopeRawPtr = mWorkerPrivate->GlobalScope(); + if (globalScopeRawPtr) { + globalScopeSentinel = do_GetWeakReference(globalScopeRawPtr); + } + MOZ_ASSERT(!globalScopeRawPtr || globalScopeSentinel); + debuggerScopeRawPtr = mWorkerPrivate->DebuggerGlobalScope(); + if (debuggerScopeRawPtr) { + debuggerScopeSentinel = do_GetWeakReference(debuggerScopeRawPtr); + } + MOZ_ASSERT(!debuggerScopeRawPtr || debuggerScopeSentinel); + + // To our best knowledge nobody should need a reference to our globals + // now (NS_ProcessPendingEvents is the last expected potential usage) + // and we can unroot them. + mWorkerPrivate->UnrootGlobalScopes(); + + // Perform a full GC until we collect the main worker global and CC, + // which should break all cycles that touch JS. + bool repeatGCCC = true; + while (repeatGCCC) { + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Shutdown, + JS::GCReason::WORKER_SHUTDOWN); + + // If we CCed something or got new events as a side effect, repeat. + repeatGCCC = mWorkerPrivate->isLastCCCollectedAnything() || + NS_HasPendingEvents(nullptr); + NS_ProcessPendingEvents(nullptr); + } + + // The worker global should be unrooted and the shutdown of cycle + // collection should break all the remaining cycles. + nsCycleCollector_shutdown(); + + // If ever the CC shutdown run caused side effects, process them. + NS_ProcessPendingEvents(nullptr); + + // Now WorkerJSContext goes out of scope. Do not use any cycle + // collectable objects nor JS after this point! + } + + // Check sentinels if we actually removed all global scope references. + // In case use the earlier set-aside raw pointers to not mess with the + // ref counting after the cycle collector has gone away. + if (globalScopeSentinel) { + MOZ_ASSERT(!globalScopeSentinel->IsAlive()); + if (NS_WARN_IF(globalScopeSentinel->IsAlive())) { + globalScopeRawPtr->NoteWorkerTerminated(); + globalScopeRawPtr = nullptr; + } + } + if (debuggerScopeSentinel) { + MOZ_ASSERT(!debuggerScopeSentinel->IsAlive()); + if (NS_WARN_IF(debuggerScopeSentinel->IsAlive())) { + debuggerScopeRawPtr->NoteWorkerTerminated(); + debuggerScopeRawPtr = nullptr; + } + } + } + + mWorkerPrivate->ScheduleDeletion(WorkerPrivate::WorkerRan); + + // It is no longer safe to touch mWorkerPrivate. + mWorkerPrivate = nullptr; + + // Now recycle this thread. + nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadSerialEventTarget(); + MOZ_ASSERT(mainTarget); + + RefPtr<FinishedRunnable> finishedRunnable = + new FinishedRunnable(std::move(mThread)); + MOZ_ALWAYS_SUCCEEDS( + mainTarget->Dispatch(finishedRunnable, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +NS_IMETHODIMP +WorkerThreadPrimaryRunnable::FinishedRunnable::Run() { + AssertIsOnMainThread(); + + SafeRefPtr<WorkerThread> thread = std::move(mThread); + if (thread->ShutdownRequired()) { + MOZ_ALWAYS_SUCCEEDS(thread->Shutdown()); + } + + return NS_OK; +} + +} // namespace workerinternals + +// This is mostly for invoking within a debugger. +void DumpRunningWorkers() { + RuntimeService* runtimeService = RuntimeService::GetService(); + if (runtimeService) { + runtimeService->DumpRunningWorkers(); + } else { + NS_WARNING("RuntimeService not found"); + } +} + +void CancelWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->CancelWorkersForWindow(aWindow); + } +} + +void FreezeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->FreezeWorkersForWindow(aWindow); + } +} + +void ThawWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->ThawWorkersForWindow(aWindow); + } +} + +void SuspendWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->SuspendWorkersForWindow(aWindow); + } +} + +void ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->ResumeWorkersForWindow(aWindow); + } +} + +void PropagateStorageAccessPermissionGrantedToWorkers( + const nsPIDOMWindowInner& aWindow) { + AssertIsOnMainThread(); + MOZ_ASSERT_IF(aWindow.GetExtantDoc(), aWindow.GetExtantDoc() + ->CookieJarSettings() + ->GetRejectThirdPartyContexts()); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->PropagateStorageAccessPermissionGranted(aWindow); + } +} + +WorkerPrivate* GetWorkerPrivateFromContext(JSContext* aCx) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCx); + + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::GetFor(aCx); + if (!ccjscx) { + return nullptr; + } + + WorkerJSContext* workerjscx = ccjscx->GetAsWorkerJSContext(); + // GetWorkerPrivateFromContext is called only for worker contexts. The + // context private is cleared early in ~CycleCollectedJSContext() and so + // GetFor() returns null above if called after ccjscx is no longer a + // WorkerJSContext. + MOZ_ASSERT(workerjscx); + return workerjscx->GetWorkerPrivate(); +} + +WorkerPrivate* GetCurrentThreadWorkerPrivate() { + if (NS_IsMainThread()) { + return nullptr; + } + + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + if (!ccjscx) { + return nullptr; + } + + WorkerJSContext* workerjscx = ccjscx->GetAsWorkerJSContext(); + // Even when GetCurrentThreadWorkerPrivate() is called on worker + // threads, the ccjscx will no longer be a WorkerJSContext if called from + // stable state events during ~CycleCollectedJSContext(). + if (!workerjscx) { + return nullptr; + } + + return workerjscx->GetWorkerPrivate(); +} + +bool IsCurrentThreadRunningWorker() { + return !NS_IsMainThread() && !!GetCurrentThreadWorkerPrivate(); +} + +bool IsCurrentThreadRunningChromeWorker() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + return wp && wp->UsesSystemPrincipal(); +} + +JSContext* GetCurrentWorkerThreadJSContext() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + return wp->GetJSContext(); +} + +JSObject* GetCurrentThreadWorkerGlobal() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + WorkerGlobalScope* scope = wp->GlobalScope(); + if (!scope) { + return nullptr; + } + return scope->GetGlobalJSObject(); +} + +JSObject* GetCurrentThreadWorkerDebuggerGlobal() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + WorkerDebuggerGlobalScope* scope = wp->DebuggerGlobalScope(); + if (!scope) { + return nullptr; + } + return scope->GetGlobalJSObject(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/RuntimeService.h b/dom/workers/RuntimeService.h new file mode 100644 index 0000000000..f51076ac14 --- /dev/null +++ b/dom/workers/RuntimeService.h @@ -0,0 +1,197 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_runtimeservice_h__ +#define mozilla_dom_workers_runtimeservice_h__ + +#include "mozilla/dom/WorkerCommon.h" + +#include "nsIObserver.h" + +#include "js/ContextOptions.h" +#include "MainThreadUtils.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/dom/workerinternals/JSSettings.h" +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" +#include "mozilla/StaticPtr.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsTArray.h" + +class nsPIDOMWindowInner; + +namespace mozilla::dom { +struct WorkerLoadInfo; +class WorkerThread; + +namespace workerinternals { + +class RuntimeService final : public nsIObserver { + struct WorkerDomainInfo { + nsCString mDomain; + nsTArray<WorkerPrivate*> mActiveWorkers; + nsTArray<WorkerPrivate*> mActiveServiceWorkers; + nsTArray<WorkerPrivate*> mQueuedWorkers; + uint32_t mChildWorkerCount; + + WorkerDomainInfo() : mActiveWorkers(1), mChildWorkerCount(0) {} + + uint32_t ActiveWorkerCount() const { + return mActiveWorkers.Length() + mChildWorkerCount; + } + + uint32_t ActiveServiceWorkerCount() const { + return mActiveServiceWorkers.Length(); + } + + bool HasNoWorkers() const { + return ActiveWorkerCount() == 0 && ActiveServiceWorkerCount() == 0; + } + }; + + mozilla::Mutex mMutex; + + // Protected by mMutex. + nsClassHashtable<nsCStringHashKey, WorkerDomainInfo> mDomainMap + MOZ_GUARDED_BY(mMutex); + + // *Not* protected by mMutex. + nsClassHashtable<nsPtrHashKey<const nsPIDOMWindowInner>, + nsTArray<WorkerPrivate*> > + mWindowMap; + + static StaticAutoPtr<workerinternals::JSSettings> sDefaultJSSettings; + + public: + struct NavigatorProperties { + nsString mAppVersion; + nsString mAppVersionOverridden; + nsString mPlatform; + nsString mPlatformOverridden; + CopyableTArray<nsString> mLanguages; + }; + + private: + NavigatorProperties mNavigatorProperties; + + // True when the observer service holds a reference to this object. + bool mObserved; + bool mShuttingDown; + bool mNavigatorPropertiesLoaded; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + static RuntimeService* GetOrCreateService(); + + static RuntimeService* GetService(); + + bool RegisterWorker(WorkerPrivate& aWorkerPrivate); + + void UnregisterWorker(WorkerPrivate& aWorkerPrivate); + + void CancelWorkersForWindow(const nsPIDOMWindowInner& aWindow); + + void FreezeWorkersForWindow(const nsPIDOMWindowInner& aWindow); + + void ThawWorkersForWindow(const nsPIDOMWindowInner& aWindow); + + void SuspendWorkersForWindow(const nsPIDOMWindowInner& aWindow); + + void ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow); + + void PropagateStorageAccessPermissionGranted( + const nsPIDOMWindowInner& aWindow); + + const NavigatorProperties& GetNavigatorProperties() const { + return mNavigatorProperties; + } + + static void GetDefaultJSSettings(workerinternals::JSSettings& aSettings) { + AssertIsOnMainThread(); + aSettings = *sDefaultJSSettings; + } + + static void SetDefaultContextOptions( + const JS::ContextOptions& aContextOptions) { + AssertIsOnMainThread(); + sDefaultJSSettings->contextOptions = aContextOptions; + } + + void UpdateAppVersionOverridePreference(const nsAString& aValue); + + void UpdatePlatformOverridePreference(const nsAString& aValue); + + void UpdateAllWorkerContextOptions(); + + void UpdateAllWorkerLanguages(const nsTArray<nsString>& aLanguages); + + static void SetDefaultJSGCSettings(JSGCParamKey aKey, + Maybe<uint32_t> aValue) { + AssertIsOnMainThread(); + sDefaultJSSettings->ApplyGCSetting(aKey, aValue); + } + + void UpdateAllWorkerMemoryParameter(JSGCParamKey aKey, + Maybe<uint32_t> aValue); + +#ifdef JS_GC_ZEAL + static void SetDefaultGCZeal(uint8_t aGCZeal, uint32_t aFrequency) { + AssertIsOnMainThread(); + sDefaultJSSettings->gcZeal = aGCZeal; + sDefaultJSSettings->gcZealFrequency = aFrequency; + } + + void UpdateAllWorkerGCZeal(); +#endif + + void SetLowMemoryStateAllWorkers(bool aState); + + void GarbageCollectAllWorkers(bool aShrinking); + + void CycleCollectAllWorkers(); + + void SendOfflineStatusChangeEventToAllWorkers(bool aIsOffline); + + void MemoryPressureAllWorkers(); + + uint32_t ClampedHardwareConcurrency(bool aShouldResistFingerprinting) const; + + void CrashIfHanging(); + + bool IsShuttingDown() const { return mShuttingDown; } + + void DumpRunningWorkers(); + + private: + RuntimeService(); + ~RuntimeService(); + + nsresult Init(); + + void Shutdown(); + + void Cleanup(); + + void AddAllTopLevelWorkersToArray(nsTArray<WorkerPrivate*>& aWorkers) + MOZ_REQUIRES(mMutex); + + nsTArray<WorkerPrivate*> GetWorkersForWindow( + const nsPIDOMWindowInner& aWindow) const; + + bool ScheduleWorker(WorkerPrivate& aWorkerPrivate); + + template <typename Func> + void BroadcastAllWorkers(const Func& aFunc); +}; + +} // namespace workerinternals +} // namespace mozilla::dom + +#endif /* mozilla_dom_workers_runtimeservice_h__ */ diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp new file mode 100644 index 0000000000..73997b2725 --- /dev/null +++ b/dom/workers/ScriptLoader.cpp @@ -0,0 +1,1932 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScriptLoader.h" + +#include <algorithm> +#include <type_traits> + +#include "mozilla/dom/RequestBinding.h" +#include "nsIChannel.h" +#include "nsIContentPolicy.h" +#include "nsIContentSecurityPolicy.h" +#include "nsICookieJarSettings.h" +#include "nsIDocShell.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIIOService.h" +#include "nsIOService.h" +#include "nsIPrincipal.h" +#include "nsIProtocolHandler.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsIStreamListenerTee.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIURI.h" +#include "nsIXPConnect.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/CompilationAndEvaluation.h" +#include "js/Exception.h" +#include "js/SourceText.h" +#include "js/TypeDecls.h" +#include "nsError.h" +#include "nsComponentManagerUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsDocShellCID.h" +#include "nsJSEnvironment.h" +#include "nsNetUtil.h" +#include "nsIPipe.h" +#include "nsIOutputStream.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsXPCOM.h" +#include "xpcpublic.h" + +#include "mozilla/AntiTrackingUtils.h" +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/Assertions.h" +#include "mozilla/Encoding.h" +#include "mozilla/LoadContext.h" +#include "mozilla/Maybe.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/dom/ClientChannelHelper.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/nsCSPService.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SerializedStackHolder.h" +#include "mozilla/dom/workerinternals/CacheLoadHandler.h" +#include "mozilla/dom/workerinternals/NetworkLoadHandler.h" +#include "mozilla/dom/workerinternals/ScriptResponseHeaderProcessor.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/UniquePtr.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" + +#define MAX_CONCURRENT_SCRIPTS 1000 + +using JS::loader::ParserMetadata; +using JS::loader::ScriptKind; +using JS::loader::ScriptLoadRequest; +using mozilla::ipc::PrincipalInfo; + +namespace mozilla::dom::workerinternals { +namespace { + +nsresult ConstructURI(const nsAString& aScriptURL, nsIURI* baseURI, + const mozilla::Encoding* aDocumentEncoding, + nsIURI** aResult) { + nsresult rv; + // Only top level workers' main script use the document charset for the + // script uri encoding. Otherwise, default encoding (UTF-8) is applied. + if (aDocumentEncoding) { + nsAutoCString charset; + aDocumentEncoding->Name(charset); + rv = NS_NewURI(aResult, aScriptURL, charset.get(), baseURI); + } else { + rv = NS_NewURI(aResult, aScriptURL, nullptr, baseURI); + } + + if (NS_FAILED(rv)) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + return NS_OK; +} + +nsresult ChannelFromScriptURL( + nsIPrincipal* principal, Document* parentDoc, WorkerPrivate* aWorkerPrivate, + nsILoadGroup* loadGroup, nsIIOService* ios, + nsIScriptSecurityManager* secMan, nsIURI* aScriptURL, + const Maybe<ClientInfo>& aClientInfo, + const Maybe<ServiceWorkerDescriptor>& aController, bool aIsMainScript, + WorkerScriptType aWorkerScriptType, nsContentPolicyType aContentPolicyType, + nsLoadFlags aLoadFlags, uint32_t aSecFlags, + nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo, + nsIChannel** aChannel) { + AssertIsOnMainThread(); + + nsresult rv; + nsCOMPtr<nsIURI> uri = aScriptURL; + + // Only use the document when its principal matches the principal of the + // current request. This means scripts fetched using the Workers' own + // principal won't inherit properties of the document, in particular the CSP. + if (parentDoc && parentDoc->NodePrincipal() != principal) { + parentDoc = nullptr; + } + + // The main service worker script should never be loaded over the network + // in this path. It should always be offlined by ServiceWorkerScriptCache. + // We assert here since this error should also be caught by the runtime + // check in CacheLoadHandler. + // + // Note, if we ever allow service worker scripts to be loaded from network + // here we need to configure the channel properly. For example, it must + // not allow redirects. + MOZ_DIAGNOSTIC_ASSERT(aContentPolicyType != + nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER); + + nsCOMPtr<nsIChannel> channel; + if (parentDoc) { + // This is the path for top level dedicated worker scripts with a document + rv = NS_NewChannel(getter_AddRefs(channel), uri, parentDoc, aSecFlags, + aContentPolicyType, + nullptr, // aPerformanceStorage + loadGroup, + nullptr, // aCallbacks + aLoadFlags, ios); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); + } else { + // This branch is used in the following cases: + // * Shared and ServiceWorkers (who do not have a doc) + // * Static Module Imports + // * ImportScripts + + // We must have a loadGroup with a load context for the principal to + // traverse the channel correctly. + + MOZ_ASSERT(loadGroup); + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal)); + + RefPtr<PerformanceStorage> performanceStorage; + nsCOMPtr<nsICSPEventListener> cspEventListener; + if (aWorkerPrivate && !aIsMainScript) { + performanceStorage = aWorkerPrivate->GetPerformanceStorage(); + cspEventListener = aWorkerPrivate->CSPEventListener(); + } + + if (aClientInfo.isSome()) { + // If we have an existing clientInfo (true for all modules and + // importScripts), we will use this branch + rv = NS_NewChannel(getter_AddRefs(channel), uri, principal, + aClientInfo.ref(), aController, aSecFlags, + aContentPolicyType, aCookieJarSettings, + performanceStorage, loadGroup, nullptr, // aCallbacks + aLoadFlags, ios); + } else { + rv = NS_NewChannel(getter_AddRefs(channel), uri, principal, aSecFlags, + aContentPolicyType, aCookieJarSettings, + performanceStorage, loadGroup, nullptr, // aCallbacks + aLoadFlags, ios); + } + + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); + + if (cspEventListener) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + rv = loadInfo->SetCspEventListener(cspEventListener); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + if (aReferrerInfo) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel); + if (httpChannel) { + rv = httpChannel->SetReferrerInfo(aReferrerInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + channel.forget(aChannel); + return rv; +} + +void LoadAllScripts(WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + const nsTArray<nsString>& aScriptURLs, bool aIsMainScript, + WorkerScriptType aWorkerScriptType, ErrorResult& aRv, + const mozilla::Encoding* aDocumentEncoding = nullptr) { + aWorkerPrivate->AssertIsOnWorkerThread(); + NS_ASSERTION(!aScriptURLs.IsEmpty(), "Bad arguments!"); + + AutoSyncLoopHolder syncLoop(aWorkerPrivate, Canceling); + nsCOMPtr<nsISerialEventTarget> syncLoopTarget = + syncLoop.GetSerialEventTarget(); + if (!syncLoopTarget) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<loader::WorkerScriptLoader> loader = + loader::WorkerScriptLoader::Create( + aWorkerPrivate, std::move(aOriginStack), syncLoopTarget, + aWorkerScriptType, aRv); + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + bool ok = loader->CreateScriptRequests(aScriptURLs, aDocumentEncoding, + aIsMainScript); + + if (!ok) { + return; + } + // Bug 1817259 - For now, we force loading the debugger script as Classic, + // even if the debugged worker is a Module. + if (aWorkerPrivate->WorkerType() == WorkerType::Module && + aWorkerScriptType != DebuggerScript) { + if (!StaticPrefs::dom_workers_modules_enabled()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + MOZ_ASSERT(aIsMainScript); + // Module Load + RefPtr<JS::loader::ScriptLoadRequest> mainScript = loader->GetMainScript(); + if (mainScript && mainScript->IsModuleRequest()) { + if (NS_FAILED(mainScript->AsModuleRequest()->StartModuleLoad())) { + return; + } + syncLoop.Run(); + return; + } + } + + if (loader->DispatchLoadScripts()) { + syncLoop.Run(); + } +} + +class ChannelGetterRunnable final : public WorkerMainThreadRunnable { + const nsAString& mScriptURL; + const WorkerType& mWorkerType; + const RequestCredentials& mCredentials; + const ClientInfo mClientInfo; + WorkerLoadInfo& mLoadInfo; + nsresult mResult; + + public: + ChannelGetterRunnable(WorkerPrivate* aParentWorker, + const nsAString& aScriptURL, + const WorkerType& aWorkerType, + const RequestCredentials& aCredentials, + WorkerLoadInfo& aLoadInfo) + : WorkerMainThreadRunnable(aParentWorker, + "ScriptLoader :: ChannelGetter"_ns), + mScriptURL(aScriptURL) + // ClientInfo should always be present since this should not be called + // if parent's status is greater than Running. + , + mWorkerType(aWorkerType), + mCredentials(aCredentials), + mClientInfo(aParentWorker->GlobalScope()->GetClientInfo().ref()), + mLoadInfo(aLoadInfo), + mResult(NS_ERROR_FAILURE) { + MOZ_ASSERT(aParentWorker); + aParentWorker->AssertIsOnWorkerThread(); + } + + virtual bool MainThreadRun() override { + AssertIsOnMainThread(); + + // Initialize the WorkerLoadInfo principal to our triggering principal + // before doing anything else. Normally we do this in the WorkerPrivate + // Constructor, but we can't do so off the main thread when creating + // a nested worker. So do it here instead. + mLoadInfo.mLoadingPrincipal = mWorkerPrivate->GetPrincipal(); + MOZ_DIAGNOSTIC_ASSERT(mLoadInfo.mLoadingPrincipal); + + mLoadInfo.mPrincipal = mLoadInfo.mLoadingPrincipal; + + // Figure out our base URI. + nsCOMPtr<nsIURI> baseURI = mWorkerPrivate->GetBaseURI(); + MOZ_ASSERT(baseURI); + + // May be null. + nsCOMPtr<Document> parentDoc = mWorkerPrivate->GetDocument(); + + mLoadInfo.mLoadGroup = mWorkerPrivate->GetLoadGroup(); + mLoadInfo.mCookieJarSettings = mWorkerPrivate->CookieJarSettings(); + + // Nested workers use default uri encoding. + nsCOMPtr<nsIURI> url; + mResult = ConstructURI(mScriptURL, baseURI, nullptr, getter_AddRefs(url)); + NS_ENSURE_SUCCESS(mResult, true); + + Maybe<ClientInfo> clientInfo; + clientInfo.emplace(mClientInfo); + + nsCOMPtr<nsIChannel> channel; + nsCOMPtr<nsIReferrerInfo> referrerInfo = + ReferrerInfo::CreateForFetch(mLoadInfo.mLoadingPrincipal, nullptr); + mLoadInfo.mReferrerInfo = + static_cast<ReferrerInfo*>(referrerInfo.get()) + ->CloneWithNewPolicy(mWorkerPrivate->GetReferrerPolicy()); + + mResult = workerinternals::ChannelFromScriptURLMainThread( + mLoadInfo.mLoadingPrincipal, parentDoc, mLoadInfo.mLoadGroup, url, + mWorkerType, mCredentials, clientInfo, + // Nested workers are always dedicated. + nsIContentPolicy::TYPE_INTERNAL_WORKER, mLoadInfo.mCookieJarSettings, + mLoadInfo.mReferrerInfo, getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(mResult, true); + + mResult = mLoadInfo.SetPrincipalsAndCSPFromChannel(channel); + NS_ENSURE_SUCCESS(mResult, true); + + mLoadInfo.mChannel = std::move(channel); + return true; + } + + nsresult GetResult() const { return mResult; } + + private: + virtual ~ChannelGetterRunnable() = default; +}; + +nsresult GetCommonSecFlags(bool aIsMainScript, nsIURI* uri, + nsIPrincipal* principal, + WorkerScriptType aWorkerScriptType, + uint32_t& secFlags) { + bool inheritAttrs = nsContentUtils::ChannelShouldInheritPrincipal( + principal, uri, true /* aInheritForAboutBlank */, + false /* aForceInherit */); + + bool isData = uri->SchemeIs("data"); + if (inheritAttrs && !isData) { + secFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL; + } + + if (aWorkerScriptType == DebuggerScript) { + // A DebuggerScript needs to be a local resource like chrome: or resource: + bool isUIResource = false; + nsresult rv = NS_URIChainHasFlags( + uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isUIResource); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isUIResource) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + secFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + } + + // Note: this is for backwards compatibility and goes against spec. + // We should find a better solution. + if (aIsMainScript && isData) { + secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + } + + return NS_OK; +} + +nsresult GetModuleSecFlags(bool aIsTopLevel, nsIPrincipal* principal, + WorkerScriptType aWorkerScriptType, nsIURI* aURI, + RequestCredentials aCredentials, + uint32_t& secFlags) { + // Implements "To fetch a single module script," + // Step 9. If destination is "worker", "sharedworker", or "serviceworker", + // and the top-level module fetch flag is set, then set request's + // mode to "same-origin". + + // Step 8. Let request be a new request whose [...] mode is "cors" [...] + secFlags = aIsTopLevel ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + + // This implements the same Cookie settings as nsContentSecurityManager's + // ComputeSecurityFlags. The main difference is the line above, Step 9, + // setting to same origin. + + if (aCredentials == RequestCredentials::Include) { + secFlags |= nsILoadInfo::nsILoadInfo::SEC_COOKIES_INCLUDE; + } else if (aCredentials == RequestCredentials::Same_origin) { + secFlags |= nsILoadInfo::nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else if (aCredentials == RequestCredentials::Omit) { + secFlags |= nsILoadInfo::nsILoadInfo::SEC_COOKIES_OMIT; + } + + return GetCommonSecFlags(aIsTopLevel, aURI, principal, aWorkerScriptType, + secFlags); +} + +nsresult GetClassicSecFlags(bool aIsMainScript, nsIURI* uri, + nsIPrincipal* principal, + WorkerScriptType aWorkerScriptType, + uint32_t& secFlags) { + secFlags = aIsMainScript + ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + + return GetCommonSecFlags(aIsMainScript, uri, principal, aWorkerScriptType, + secFlags); +} + +} // anonymous namespace + +namespace loader { + +class ScriptExecutorRunnable final : public MainThreadWorkerSyncRunnable { + RefPtr<WorkerScriptLoader> mScriptLoader; + const Span<RefPtr<ThreadSafeRequestHandle>> mLoadedRequests; + + public: + ScriptExecutorRunnable(WorkerScriptLoader* aScriptLoader, + WorkerPrivate* aWorkerPrivate, + nsISerialEventTarget* aSyncLoopTarget, + Span<RefPtr<ThreadSafeRequestHandle>> aLoadedRequests); + + private: + ~ScriptExecutorRunnable() = default; + + virtual bool IsDebuggerRunnable() const override; + + virtual bool PreRun(WorkerPrivate* aWorkerPrivate) override; + + bool ProcessModuleScript(JSContext* aCx, WorkerPrivate* aWorkerPrivate); + + bool ProcessClassicScripts(JSContext* aCx, WorkerPrivate* aWorkerPrivate); + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override; + + nsresult Cancel() override; +}; + +static bool EvaluateSourceBuffer(JSContext* aCx, JS::Handle<JSScript*> aScript, + JS::loader::ClassicScript* aClassicScript) { + if (aClassicScript) { + aClassicScript->AssociateWithScript(aScript); + } + + JS::Rooted<JS::Value> unused(aCx); + return JS_ExecuteScript(aCx, aScript, &unused); +} + +WorkerScriptLoader::WorkerScriptLoader( + UniquePtr<SerializedStackHolder> aOriginStack, + nsISerialEventTarget* aSyncLoopTarget, WorkerScriptType aWorkerScriptType, + ErrorResult& aRv) + : mOriginStack(std::move(aOriginStack)), + mSyncLoopTarget(aSyncLoopTarget), + mWorkerScriptType(aWorkerScriptType), + mRv(aRv), + mLoadingModuleRequestCount(0), + mCleanedUp(false), + mCleanUpLock("cleanUpLock") {} + +already_AddRefed<WorkerScriptLoader> WorkerScriptLoader::Create( + WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + nsISerialEventTarget* aSyncLoopTarget, WorkerScriptType aWorkerScriptType, + ErrorResult& aRv) { + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<WorkerScriptLoader> self = new WorkerScriptLoader( + std::move(aOriginStack), aSyncLoopTarget, aWorkerScriptType, aRv); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + aWorkerPrivate, "WorkerScriptLoader::Create", [self]() { + // Requests that are in flight are covered by the worker references + // in DispatchLoadScript(s), so we do not need to do additional + // cleanup, but just in case we are ready/aborted we can try to + // shutdown here, too. + self->TryShutdown(); + }); + + if (workerRef) { + self->mWorkerRef = new ThreadSafeWorkerRef(workerRef); + } else { + self->mRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsIGlobalObject* global = self->GetGlobal(); + self->mController = global->GetController(); + + if (!StaticPrefs::dom_workers_modules_enabled()) { + return self.forget(); + } + + // Set up the module loader, if it has not been initialzied yet. + if (!aWorkerPrivate->IsServiceWorker()) { + self->InitModuleLoader(); + } + + return self.forget(); +} + +ScriptLoadRequest* WorkerScriptLoader::GetMainScript() { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + ScriptLoadRequest* request = mLoadingRequests.getFirst(); + if (request->GetWorkerLoadContext()->IsTopLevel()) { + return request; + } + return nullptr; +} + +void WorkerScriptLoader::InitModuleLoader() { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + if (GetGlobal()->GetModuleLoader(nullptr)) { + return; + } + RefPtr<WorkerModuleLoader> moduleLoader = + new WorkerModuleLoader(this, GetGlobal()); + if (mWorkerScriptType == WorkerScript) { + mWorkerRef->Private()->GlobalScope()->InitModuleLoader(moduleLoader); + return; + } + mWorkerRef->Private()->DebuggerGlobalScope()->InitModuleLoader(moduleLoader); +} + +bool WorkerScriptLoader::CreateScriptRequests( + const nsTArray<nsString>& aScriptURLs, + const mozilla::Encoding* aDocumentEncoding, bool aIsMainScript) { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + // If a worker has been loaded as a module worker, ImportScripts calls are + // disallowed -- then the operation is invalid. + // + // 10.3.1 Importing scripts and libraries. + // Step 1. If worker global scope's type is "module", throw a TypeError + // exception. + // + // Also, for now, the debugger script is always loaded as Classic, + // even if the debugged worker is a Module. We still want to allow + // it to use importScripts. + if (mWorkerRef->Private()->WorkerType() == WorkerType::Module && + !aIsMainScript && !IsDebuggerScript()) { + // This should only run for non-main scripts, as only these are + // importScripts + mRv.ThrowTypeError( + "Using `ImportScripts` inside a Module Worker is " + "disallowed."); + return false; + } + for (const nsString& scriptURL : aScriptURLs) { + RefPtr<ScriptLoadRequest> request = + CreateScriptLoadRequest(scriptURL, aDocumentEncoding, aIsMainScript); + if (!request) { + return false; + } + mLoadingRequests.AppendElement(request); + } + + return true; +} + +nsTArray<RefPtr<ThreadSafeRequestHandle>> WorkerScriptLoader::GetLoadingList() { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + nsTArray<RefPtr<ThreadSafeRequestHandle>> list; + for (ScriptLoadRequest* req = mLoadingRequests.getFirst(); req; + req = req->getNext()) { + RefPtr<ThreadSafeRequestHandle> handle = + new ThreadSafeRequestHandle(req, mSyncLoopTarget.get()); + list.AppendElement(handle.forget()); + } + return list; +} + +bool WorkerScriptLoader::IsDynamicImport(ScriptLoadRequest* aRequest) { + return aRequest->IsModuleRequest() && + aRequest->AsModuleRequest()->IsDynamicImport(); +} + +nsContentPolicyType WorkerScriptLoader::GetContentPolicyType( + ScriptLoadRequest* aRequest) { + if (aRequest->GetWorkerLoadContext()->IsTopLevel()) { + // Implements https://html.spec.whatwg.org/#worker-processing-model + // Step 13: Let destination be "sharedworker" if is shared is true, and + // "worker" otherwise. + return mWorkerRef->Private()->ContentPolicyType(); + } + if (aRequest->IsModuleRequest()) { + if (aRequest->AsModuleRequest()->IsDynamicImport()) { + return nsIContentPolicy::TYPE_INTERNAL_MODULE; + } + + // Implements the destination for Step 14 in + // https://html.spec.whatwg.org/#worker-processing-model + // + // We need a special subresource type in order to correctly implement + // the graph fetch, where the destination is set to "worker" or + // "sharedworker". + return nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE; + } + // For script imported in worker's importScripts(). + return nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS; +} + +already_AddRefed<ScriptLoadRequest> WorkerScriptLoader::CreateScriptLoadRequest( + const nsString& aScriptURL, const mozilla::Encoding* aDocumentEncoding, + bool aIsMainScript) { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + WorkerLoadContext::Kind kind = + WorkerLoadContext::GetKind(aIsMainScript, IsDebuggerScript()); + + Maybe<ClientInfo> clientInfo = GetGlobal()->GetClientInfo(); + + // (For non-serviceworkers, this variable does not matter, but false best + // captures their behavior.) + bool onlyExistingCachedResourcesAllowed = false; + if (mWorkerRef->Private()->IsServiceWorker()) { + // https://w3c.github.io/ServiceWorker/#importscripts step 4: + // > 4. If serviceWorker’s state is not "parsed" or "installing": + // > 1. Return map[url] if it exists and a network error otherwise. + // + // So if our state is beyond installing, it's too late to make a request + // that would perform a new fetch which would be cached. + onlyExistingCachedResourcesAllowed = + mWorkerRef->Private()->GetServiceWorkerDescriptor().State() > + ServiceWorkerState::Installing; + } + RefPtr<WorkerLoadContext> loadContext = new WorkerLoadContext( + kind, clientInfo, this, onlyExistingCachedResourcesAllowed); + + // Create ScriptLoadRequests for this WorkerScriptLoader + ReferrerPolicy referrerPolicy = mWorkerRef->Private()->GetReferrerPolicy(); + + // Only top level workers' main script use the document charset for the + // script uri encoding. Otherwise, default encoding (UTF-8) is applied. + MOZ_ASSERT_IF(bool(aDocumentEncoding), + aIsMainScript && !mWorkerRef->Private()->GetParent()); + nsCOMPtr<nsIURI> baseURI = aIsMainScript ? GetInitialBaseURI() : GetBaseURI(); + nsCOMPtr<nsIURI> uri; + bool setErrorResult = false; + nsresult rv = + ConstructURI(aScriptURL, baseURI, aDocumentEncoding, getter_AddRefs(uri)); + // If we failed to construct the URI, handle it in the LoadContext so it is + // thrown in the right order. + if (NS_WARN_IF(NS_FAILED(rv))) { + setErrorResult = true; + loadContext->mLoadResult = rv; + } + + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-worker-script + // Step 2.5. Let script be the result [...] and the default classic script + // fetch options. + // + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-worklet/module-worker-script-graph + // Step 1. Let options be a script fetch options whose cryptographic nonce is + // the empty string, integrity metadata is the empty string, parser metadata + // is "not-parser-inserted", credentials mode is credentials mode, referrer + // policy is the empty string, and fetch priority is "auto". + RefPtr<ScriptFetchOptions> fetchOptions = new ScriptFetchOptions( + CORSMode::CORS_NONE, /* aNonce = */ u""_ns, RequestPriority::Auto, + ParserMetadata::NotParserInserted, nullptr); + + RefPtr<ScriptLoadRequest> request = nullptr; + // Bug 1817259 - For now the debugger scripts are always loaded a Classic. + if (mWorkerRef->Private()->WorkerType() == WorkerType::Classic || + IsDebuggerScript()) { + request = new ScriptLoadRequest(ScriptKind::eClassic, uri, referrerPolicy, + fetchOptions, SRIMetadata(), + nullptr, // mReferrer + loadContext); + } else { + // Implements part of "To fetch a worklet/module worker script graph" + // including, setting up the request with a credentials mode, + // destination. + + // Step 1. Let options be a script fetch options. + // We currently don't track credentials in our ScriptFetchOptions + // implementation, so we are defaulting the fetchOptions object defined + // above. This behavior is handled fully in GetModuleSecFlags. + + if (!StaticPrefs::dom_workers_modules_enabled()) { + mRv.ThrowTypeError("Modules in workers are currently disallowed."); + return nullptr; + } + RefPtr<WorkerModuleLoader::ModuleLoaderBase> moduleLoader = + GetGlobal()->GetModuleLoader(nullptr); + + // Implements the referrer for "To fetch a single module script" + // Our implementation does not have a "client" as a referrer. + // However, when client is resolved (per 8.3. Determine request’s + // Referrer in + // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer) + // This should result in the referrer source being the creation URL. + // + // In subresource modules, the referrer is the importing script. + nsCOMPtr<nsIURI> referrer = + mWorkerRef->Private()->GetReferrerInfo()->GetOriginalReferrer(); + + // Part of Step 2. This sets the Top-level flag to true + request = new ModuleLoadRequest( + uri, referrerPolicy, fetchOptions, SRIMetadata(), referrer, loadContext, + true, /* is top level */ + false, /* is dynamic import */ + moduleLoader, ModuleLoadRequest::NewVisitedSetForTopLevelImport(uri), + nullptr); + } + + // Set the mURL, it will be used for error handling and debugging. + request->mURL = NS_ConvertUTF16toUTF8(aScriptURL); + + if (setErrorResult) { + request->SetPendingFetchingError(); + } else { + request->NoCacheEntryFound(); + } + + return request.forget(); +} + +bool WorkerScriptLoader::DispatchLoadScript(ScriptLoadRequest* aRequest) { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + IncreaseLoadingModuleRequestCount(); + + nsTArray<RefPtr<ThreadSafeRequestHandle>> scriptLoadList; + RefPtr<ThreadSafeRequestHandle> handle = + new ThreadSafeRequestHandle(aRequest, mSyncLoopTarget.get()); + scriptLoadList.AppendElement(handle.forget()); + + RefPtr<ScriptLoaderRunnable> runnable = + new ScriptLoaderRunnable(this, std::move(scriptLoadList)); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + mWorkerRef->Private(), "WorkerScriptLoader::DispatchLoadScript", + [runnable]() { + NS_DispatchToMainThread(NewRunnableMethod( + "ScriptLoaderRunnable::CancelMainThreadWithBindingAborted", + runnable, + &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted)); + }); + + if (NS_FAILED(NS_DispatchToMainThread(runnable))) { + NS_ERROR("Failed to dispatch!"); + mRv.Throw(NS_ERROR_FAILURE); + return false; + } + return true; +} + +bool WorkerScriptLoader::DispatchLoadScripts() { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + nsTArray<RefPtr<ThreadSafeRequestHandle>> scriptLoadList = GetLoadingList(); + + RefPtr<ScriptLoaderRunnable> runnable = + new ScriptLoaderRunnable(this, std::move(scriptLoadList)); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + mWorkerRef->Private(), "WorkerScriptLoader::DispatchLoadScripts", + [runnable]() { + NS_DispatchToMainThread(NewRunnableMethod( + "ScriptLoaderRunnable::CancelMainThreadWithBindingAborted", + runnable, + &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted)); + }); + + if (NS_FAILED(NS_DispatchToMainThread(runnable))) { + NS_ERROR("Failed to dispatch!"); + mRv.Throw(NS_ERROR_FAILURE); + return false; + } + return true; +} + +nsIURI* WorkerScriptLoader::GetInitialBaseURI() { + MOZ_ASSERT(mWorkerRef->Private()); + nsIURI* baseURI; + WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); + if (parentWorker) { + baseURI = parentWorker->GetBaseURI(); + } else { + // May be null. + baseURI = mWorkerRef->Private()->GetBaseURI(); + } + + return baseURI; +} + +nsIURI* WorkerScriptLoader::GetBaseURI() const { + MOZ_ASSERT(mWorkerRef); + nsIURI* baseURI; + baseURI = mWorkerRef->Private()->GetBaseURI(); + NS_ASSERTION(baseURI, "Should have been set already!"); + + return baseURI; +} + +nsIGlobalObject* WorkerScriptLoader::GetGlobal() { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + return mWorkerScriptType == WorkerScript + ? static_cast<nsIGlobalObject*>( + mWorkerRef->Private()->GlobalScope()) + : mWorkerRef->Private()->DebuggerGlobalScope(); +} + +void WorkerScriptLoader::MaybeMoveToLoadedList(ScriptLoadRequest* aRequest) { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + // Only set to ready for regular scripts. Module loader will set the script to + // ready if it is a Module Request. + if (!aRequest->IsModuleRequest()) { + aRequest->SetReady(); + } + + // If the request is not in a list, we are in an illegal state. + MOZ_RELEASE_ASSERT(aRequest->isInList()); + + while (!mLoadingRequests.isEmpty()) { + ScriptLoadRequest* request = mLoadingRequests.getFirst(); + // We need to move requests in post order. If prior requests have not + // completed, delay execution. + if (!request->IsFinished()) { + break; + } + + RefPtr<ScriptLoadRequest> req = mLoadingRequests.Steal(request); + mLoadedRequests.AppendElement(req); + } +} + +bool WorkerScriptLoader::StoreCSP() { + // We must be on the same worker as we started on. + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + if (!mWorkerRef->Private()->GetJSContext()) { + return false; + } + + MOZ_ASSERT(!mRv.Failed()); + + // Move the CSP from the workerLoadInfo in the corresponding Client + // where the CSP code expects it! + mWorkerRef->Private()->StoreCSPOnClient(); + return true; +} + +bool WorkerScriptLoader::ProcessPendingRequests(JSContext* aCx) { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + // Don't run if something else has already failed. + if (mExecutionAborted) { + mLoadedRequests.CancelRequestsAndClear(); + TryShutdown(); + return true; + } + + // If nothing else has failed, our ErrorResult better not be a failure + // either. + MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); + + // Slightly icky action at a distance, but there's no better place to stash + // this value, really. + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + MOZ_ASSERT(global); + + while (!mLoadedRequests.isEmpty()) { + RefPtr<ScriptLoadRequest> req = mLoadedRequests.StealFirst(); + // We don't have a ProcessRequest method (like we do on the DOM), as there + // isn't much processing that we need to do per request that isn't related + // to evaluation (the processsing done for the DOM is handled in + // DataRecievedFrom{Cache,Network} for workers. + // So, this inner loop calls EvaluateScript directly. This will change + // once modules are introduced as we will have some extra work to do. + if (!EvaluateScript(aCx, req)) { + req->Cancel(); + mExecutionAborted = true; + WorkerLoadContext* loadContext = req->GetWorkerLoadContext(); + mMutedErrorFlag = loadContext->mMutedErrorFlag.valueOr(true); + mLoadedRequests.CancelRequestsAndClear(); + break; + } + } + + TryShutdown(); + return true; +} + +nsresult WorkerScriptLoader::LoadScript( + ThreadSafeRequestHandle* aRequestHandle) { + AssertIsOnMainThread(); + + WorkerLoadContext* loadContext = aRequestHandle->GetContext(); + ScriptLoadRequest* request = aRequestHandle->GetRequest(); + MOZ_ASSERT_IF(loadContext->IsTopLevel(), !IsDebuggerScript()); + + // The URL passed to us for loading was invalid, stop loading at this point. + if (loadContext->mLoadResult != NS_ERROR_NOT_INITIALIZED) { + return loadContext->mLoadResult; + } + + WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); + + // For JavaScript debugging, the devtools server must run on the same + // thread as the debuggee, indicating the worker uses content principal. + // However, in Bug 863246, web content will no longer be able to load + // resource:// URIs by default, so we need system principal to load + // debugger scripts. + nsIPrincipal* principal = (IsDebuggerScript()) + ? nsContentUtils::GetSystemPrincipal() + : mWorkerRef->Private()->GetPrincipal(); + + nsCOMPtr<nsILoadGroup> loadGroup = mWorkerRef->Private()->GetLoadGroup(); + MOZ_DIAGNOSTIC_ASSERT(principal); + + NS_ENSURE_TRUE(NS_LoadGroupMatchesPrincipal(loadGroup, principal), + NS_ERROR_FAILURE); + + // May be null. + nsCOMPtr<Document> parentDoc = mWorkerRef->Private()->GetDocument(); + + nsCOMPtr<nsIChannel> channel; + if (loadContext->IsTopLevel()) { + // May be null. + channel = mWorkerRef->Private()->ForgetWorkerChannel(); + } + + nsCOMPtr<nsIIOService> ios(do_GetIOService()); + + nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(secMan, "This should never be null!"); + + nsresult& rv = loadContext->mLoadResult; + + nsLoadFlags loadFlags = mWorkerRef->Private()->GetLoadFlags(); + + // Get the top-level worker. + WorkerPrivate* topWorkerPrivate = mWorkerRef->Private(); + WorkerPrivate* parent = topWorkerPrivate->GetParent(); + while (parent) { + topWorkerPrivate = parent; + parent = topWorkerPrivate->GetParent(); + } + + // If the top-level worker is a dedicated worker and has a window, and the + // window has a docshell, the caching behavior of this worker should match + // that of that docshell. + if (topWorkerPrivate->IsDedicatedWorker()) { + nsCOMPtr<nsPIDOMWindowInner> window = topWorkerPrivate->GetWindow(); + if (window) { + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + if (docShell) { + nsresult rv = docShell->GetDefaultLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + if (!channel) { + nsCOMPtr<nsIReferrerInfo> referrerInfo; + uint32_t secFlags; + if (request->IsModuleRequest()) { + // https://fetch.spec.whatwg.org/#concept-main-fetch + // Step 8. If request’s referrer policy is the empty string, then set + // request’s referrer policy to request’s policy container’s + // referrer policy. + ReferrerPolicy policy = + request->ReferrerPolicy() == ReferrerPolicy::_empty + ? mWorkerRef->Private()->GetReferrerPolicy() + : request->ReferrerPolicy(); + + referrerInfo = new ReferrerInfo(request->mReferrer, policy); + + // https://html.spec.whatwg.org/multipage/webappapis.html#default-classic-script-fetch-options + // The default classic script fetch options are a script fetch options + // whose ... credentials mode is "same-origin", .... + RequestCredentials credentials = + mWorkerRef->Private()->WorkerType() == WorkerType::Classic + ? RequestCredentials::Same_origin + : mWorkerRef->Private()->WorkerCredentials(); + + rv = GetModuleSecFlags(loadContext->IsTopLevel(), principal, + mWorkerScriptType, request->mURI, credentials, + secFlags); + } else { + referrerInfo = ReferrerInfo::CreateForFetch(principal, nullptr); + if (parentWorker && !loadContext->IsTopLevel()) { + referrerInfo = + static_cast<ReferrerInfo*>(referrerInfo.get()) + ->CloneWithNewPolicy(parentWorker->GetReferrerPolicy()); + } + rv = GetClassicSecFlags(loadContext->IsTopLevel(), request->mURI, + principal, mWorkerScriptType, secFlags); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsContentPolicyType contentPolicyType = GetContentPolicyType(request); + + rv = ChannelFromScriptURL( + principal, parentDoc, mWorkerRef->Private(), loadGroup, ios, secMan, + request->mURI, loadContext->mClientInfo, mController, + loadContext->IsTopLevel(), mWorkerScriptType, contentPolicyType, + loadFlags, secFlags, mWorkerRef->Private()->CookieJarSettings(), + referrerInfo, getter_AddRefs(channel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Associate any originating stack with the channel. + if (!mOriginStackJSON.IsEmpty()) { + NotifyNetworkMonitorAlternateStack(channel, mOriginStackJSON); + } + + // We need to know which index we're on in OnStreamComplete so we know + // where to put the result. + RefPtr<NetworkLoadHandler> listener = + new NetworkLoadHandler(this, aRequestHandle); + + RefPtr<ScriptResponseHeaderProcessor> headerProcessor = nullptr; + + // For each debugger script, a non-debugger script load of the same script + // should have occured prior that processed the headers. + if (!IsDebuggerScript()) { + headerProcessor = MakeRefPtr<ScriptResponseHeaderProcessor>( + mWorkerRef->Private(), + loadContext->IsTopLevel() && !IsDynamicImport(request), + GetContentPolicyType(request) == + nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS); + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), listener, headerProcessor); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (loadContext->IsTopLevel()) { + MOZ_DIAGNOSTIC_ASSERT(loadContext->mClientInfo.isSome()); + + // In order to get the correct foreign partitioned prinicpal, we need to + // set the `IsThirdPartyContextToTopWindow` to the channel's loadInfo. + // This flag reflects the fact that if the worker is created under a + // third-party context. + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + loadInfo->SetIsThirdPartyContextToTopWindow( + mWorkerRef->Private()->IsThirdPartyContextToTopWindow()); + + Maybe<ClientInfo> clientInfo; + clientInfo.emplace(loadContext->mClientInfo.ref()); + rv = AddClientChannelHelper(channel, std::move(clientInfo), + Maybe<ClientInfo>(), + mWorkerRef->Private()->HybridEventTarget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + nsILoadInfo::CrossOriginEmbedderPolicy respectedCOEP = + mWorkerRef->Private()->GetEmbedderPolicy(); + if (mWorkerRef->Private()->IsDedicatedWorker() && + respectedCOEP == nsILoadInfo::EMBEDDER_POLICY_NULL) { + respectedCOEP = mWorkerRef->Private()->GetOwnerEmbedderPolicy(); + } + + nsCOMPtr<nsILoadInfo> channelLoadInfo = channel->LoadInfo(); + channelLoadInfo->SetLoadingEmbedderPolicy(respectedCOEP); + } + + if (loadContext->mCacheStatus != WorkerLoadContext::ToBeCached) { + rv = channel->AsyncOpen(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsCOMPtr<nsIOutputStream> writer; + + // In case we return early. + loadContext->mCacheStatus = WorkerLoadContext::Cancel; + + NS_NewPipe(getter_AddRefs(loadContext->mCacheReadStream), + getter_AddRefs(writer), 0, + UINT32_MAX, // unlimited size to avoid writer WOULD_BLOCK case + true, false); // non-blocking reader, blocking writer + + nsCOMPtr<nsIStreamListenerTee> tee = + do_CreateInstance(NS_STREAMLISTENERTEE_CONTRACTID); + rv = tee->Init(loader, writer, listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsresult rv = channel->AsyncOpen(tee); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + loadContext->mChannel.swap(channel); + + return NS_OK; +} + +nsresult WorkerScriptLoader::FillCompileOptionsForRequest( + JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle<JSScript*> aIntroductionScript) { + // The full URL shouldn't be exposed to the debugger. See Bug 1634872 + aOptions->setFileAndLine(aRequest->mURL.get(), 1); + aOptions->setNoScriptRval(true); + + aOptions->setMutedErrors( + aRequest->GetWorkerLoadContext()->mMutedErrorFlag.value()); + + if (aRequest->mSourceMapURL) { + aOptions->setSourceMapURL(aRequest->mSourceMapURL->get()); + } + + return NS_OK; +} + +bool WorkerScriptLoader::EvaluateScript(JSContext* aCx, + ScriptLoadRequest* aRequest) { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + MOZ_ASSERT(!IsDynamicImport(aRequest)); + + WorkerLoadContext* loadContext = aRequest->GetWorkerLoadContext(); + + NS_ASSERTION(!loadContext->mChannel, "Should no longer have a channel!"); + NS_ASSERTION(aRequest->IsFinished(), "Should be scheduled!"); + + MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); + mRv.MightThrowJSException(); + if (NS_FAILED(loadContext->mLoadResult)) { + ReportErrorToConsole(aRequest, loadContext->mLoadResult); + return false; + } + + // If this is a top level script that succeeded, then mark the + // Client execution ready and possible controlled by a service worker. + if (loadContext->IsTopLevel()) { + if (mController.isSome()) { + MOZ_ASSERT(mWorkerScriptType == WorkerScript, + "Debugger clients can't be controlled."); + mWorkerRef->Private()->GlobalScope()->Control(mController.ref()); + } + mWorkerRef->Private()->ExecutionReady(); + } + + if (aRequest->IsModuleRequest()) { + // Only the top level module of the module graph will be executed from here, + // the rest will be executed from SpiderMonkey as part of the execution of + // the module graph. + MOZ_ASSERT(aRequest->IsTopLevel()); + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + if (!request->mModuleScript) { + return false; + } + + // https://html.spec.whatwg.org/#run-a-worker + // if script's error to rethrow is non-null, then: + // Queue a global task on the DOM manipulation task source given worker's + // relevant global object to fire an event named error at worker. + // + // The event will be dispatched in CompileScriptRunnable. + if (request->mModuleScript->HasParseError()) { + // Here we assign an error code that is not a JS Exception, so + // CompileRunnable can dispatch the event. + mRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return false; + } + + // Implements To fetch a worklet/module worker script graph + // Step 5. Fetch the descendants of and link result. + if (!request->InstantiateModuleGraph()) { + return false; + } + + if (request->mModuleScript->HasErrorToRethrow()) { + // See the comments when we check HasParseError() above. + mRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return false; + } + + nsresult rv = request->EvaluateModule(); + return NS_SUCCEEDED(rv); + } + + JS::CompileOptions options(aCx); + // The introduction script is used by the DOM script loader as a way + // to fill the Debugger Metadata for the JS Execution context. We don't use + // the JS Execution context as we are not making use of async compilation + // (delegation to another worker to produce bytecode or compile a string to a + // JSScript), so it is not used in this context. + JS::Rooted<JSScript*> unusedIntroductionScript(aCx); + nsresult rv = FillCompileOptionsForRequest(aCx, aRequest, &options, + &unusedIntroductionScript); + + MOZ_ASSERT(NS_SUCCEEDED(rv), "Filling compile options should not fail"); + + // Our ErrorResult still shouldn't be a failure. + MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); + + // Get the source text. + ScriptLoadRequest::MaybeSourceText maybeSource; + rv = aRequest->GetScriptSource(aCx, &maybeSource, + aRequest->mLoadContext.get()); + if (NS_FAILED(rv)) { + mRv.StealExceptionFromJSContext(aCx); + return false; + } + + RefPtr<JS::loader::ClassicScript> classicScript = nullptr; + if (StaticPrefs::dom_workers_modules_enabled() && + !mWorkerRef->Private()->IsServiceWorker()) { + // We need a LoadedScript to be associated with the JSScript in order to + // correctly resolve the referencing private for dynamic imports. In turn + // this allows us to correctly resolve the BaseURL. + // + // Dynamic import is disallowed on service workers. Additionally, causes + // crashes because the life cycle isn't completed for service workers. To + // keep things simple, we don't create a classic script for ServiceWorkers. + // If this changes then we will need to ensure that the reference that is + // held is released appropriately. + nsCOMPtr<nsIURI> requestBaseURI; + if (loadContext->mMutedErrorFlag.valueOr(false)) { + NS_NewURI(getter_AddRefs(requestBaseURI), "about:blank"_ns); + } else { + requestBaseURI = aRequest->mBaseURL; + } + MOZ_ASSERT(aRequest->mLoadedScript->IsClassicScript()); + MOZ_ASSERT(aRequest->mLoadedScript->GetFetchOptions() == + aRequest->mFetchOptions); + aRequest->mLoadedScript->SetBaseURL(requestBaseURI); + classicScript = aRequest->mLoadedScript->AsClassicScript(); + } + + JS::Rooted<JSScript*> script(aCx); + script = aRequest->IsUTF8Text() + ? JS::Compile(aCx, options, + maybeSource.ref<JS::SourceText<Utf8Unit>>()) + : JS::Compile(aCx, options, + maybeSource.ref<JS::SourceText<char16_t>>()); + if (!script) { + if (loadContext->IsTopLevel()) { + // This is a top-level worker script, + // + // https://html.spec.whatwg.org/#run-a-worker + // If script is null or if script's error to rethrow is non-null, then: + // Queue a global task on the DOM manipulation task source given + // worker's relevant global object to fire an event named error at + // worker. + JS_ClearPendingException(aCx); + mRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + } else { + // This is a script which is loaded by importScripts(). + // + // https://html.spec.whatwg.org/#import-scripts-into-worker-global-scope + // For each url in the resulting URL records: + // Fetch a classic worker-imported script given url and settings object, + // passing along performFetch if provided. If this succeeds, let script + // be the result. Otherwise, rethrow the exception. + mRv.StealExceptionFromJSContext(aCx); + } + + return false; + } + + bool successfullyEvaluated = EvaluateSourceBuffer(aCx, script, classicScript); + if (aRequest->IsCanceled()) { + return false; + } + if (!successfullyEvaluated) { + mRv.StealExceptionFromJSContext(aCx); + return false; + } + // steal the loadContext so that the cycle is broken and cycle collector can + // collect the scriptLoadRequest. + return true; +} + +void WorkerScriptLoader::TryShutdown() { + { + MutexAutoLock lock(CleanUpLock()); + if (CleanedUp()) { + return; + } + } + + if (AllScriptsExecuted() && AllModuleRequestsLoaded()) { + ShutdownScriptLoader(!mExecutionAborted, mMutedErrorFlag); + } +} + +void WorkerScriptLoader::ShutdownScriptLoader(bool aResult, bool aMutedError) { + MOZ_ASSERT(AllScriptsExecuted()); + MOZ_ASSERT(AllModuleRequestsLoaded()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + if (!aResult) { + // At this point there are two possibilities: + // + // 1) mRv.Failed(). In that case we just want to leave it + // as-is, except if it has a JS exception and we need to mute JS + // exceptions. In that case, we log the exception without firing any + // events and then replace it on the ErrorResult with a NetworkError, + // per spec. + // + // 2) mRv succeeded. As far as I can tell, this can only + // happen when loading the main worker script and + // GetOrCreateGlobalScope() fails or if ScriptExecutorRunnable::Cancel + // got called. Does it matter what we throw in this case? I'm not + // sure... + if (mRv.Failed()) { + if (aMutedError && mRv.IsJSException()) { + LogExceptionToConsole(mWorkerRef->Private()->GetJSContext(), + mWorkerRef->Private()); + mRv.Throw(NS_ERROR_DOM_NETWORK_ERR); + } + } else { + mRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + } + } + + // Lock, shutdown, and cleanup state. After this the Loader is closed. + { + MutexAutoLock lock(CleanUpLock()); + + if (CleanedUp()) { + return; + } + + mWorkerRef->Private()->AssertIsOnWorkerThread(); + // Module loader doesn't use sync loop for dynamic import + if (mSyncLoopTarget) { + mWorkerRef->Private()->MaybeStopSyncLoop( + mSyncLoopTarget, aResult ? NS_OK : NS_ERROR_FAILURE); + mSyncLoopTarget = nullptr; + } + + // Signal cleanup + mCleanedUp = true; + + // Allow worker shutdown. + mWorkerRef = nullptr; + } +} + +void WorkerScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const { + nsAutoString url = NS_ConvertUTF8toUTF16(aRequest->mURL); + workerinternals::ReportLoadError(mRv, aResult, url); +} + +void WorkerScriptLoader::LogExceptionToConsole(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_ASSERT(mRv.IsJSException()); + + JS::Rooted<JS::Value> exn(aCx); + if (!ToJSValue(aCx, std::move(mRv), &exn)) { + return; + } + + // Now the exception state should all be in exn. + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + MOZ_ASSERT(!mRv.Failed()); + + JS::ExceptionStack exnStack(aCx, exn, nullptr); + JS::ErrorReportBuilder report(aCx); + if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + JS_ClearPendingException(aCx); + return; + } + + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + xpcReport->Init(report.report(), report.toStringResult().c_str(), + aWorkerPrivate->IsChromeWorker(), aWorkerPrivate->WindowID()); + + RefPtr<AsyncErrorReporter> r = new AsyncErrorReporter(xpcReport); + NS_DispatchToMainThread(r); +} + +bool WorkerScriptLoader::AllModuleRequestsLoaded() const { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + return mLoadingModuleRequestCount == 0; +} + +void WorkerScriptLoader::IncreaseLoadingModuleRequestCount() { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + ++mLoadingModuleRequestCount; +} + +void WorkerScriptLoader::DecreaseLoadingModuleRequestCount() { + mWorkerRef->Private()->AssertIsOnWorkerThread(); + --mLoadingModuleRequestCount; +} + +NS_IMPL_ISUPPORTS(ScriptLoaderRunnable, nsIRunnable, nsINamed) + +NS_IMPL_ISUPPORTS(WorkerScriptLoader, nsINamed) + +ScriptLoaderRunnable::ScriptLoaderRunnable( + WorkerScriptLoader* aScriptLoader, + nsTArray<RefPtr<ThreadSafeRequestHandle>> aLoadingRequests) + : mScriptLoader(aScriptLoader), + mWorkerRef(aScriptLoader->mWorkerRef), + mLoadingRequests(std::move(aLoadingRequests)), + mCancelMainThread(Nothing()) { + MOZ_ASSERT(aScriptLoader); +} + +nsresult ScriptLoaderRunnable::Run() { + AssertIsOnMainThread(); + + // Convert the origin stack to JSON (which must be done on the main + // thread) explicitly, so that we can use the stack to notify the net + // monitor about every script we load. We do this, rather than pass + // the stack directly to the netmonitor, in order to be able to use this + // for all subsequent scripts. + if (mScriptLoader->mOriginStack && + mScriptLoader->mOriginStackJSON.IsEmpty()) { + ConvertSerializedStackToJSON(std::move(mScriptLoader->mOriginStack), + mScriptLoader->mOriginStackJSON); + } + + if (!mWorkerRef->Private()->IsServiceWorker() || + mScriptLoader->IsDebuggerScript()) { + for (ThreadSafeRequestHandle* handle : mLoadingRequests) { + handle->mRunnable = this; + } + + for (ThreadSafeRequestHandle* handle : mLoadingRequests) { + nsresult rv = mScriptLoader->LoadScript(handle); + if (NS_WARN_IF(NS_FAILED(rv))) { + LoadingFinished(handle, rv); + CancelMainThread(rv); + return rv; + } + } + + return NS_OK; + } + + MOZ_ASSERT(!mCacheCreator); + mCacheCreator = new CacheCreator(mWorkerRef->Private()); + + for (ThreadSafeRequestHandle* handle : mLoadingRequests) { + handle->mRunnable = this; + WorkerLoadContext* loadContext = handle->GetContext(); + mCacheCreator->AddLoader(MakeNotNull<RefPtr<CacheLoadHandler>>( + mWorkerRef, handle, loadContext->IsTopLevel(), + loadContext->mOnlyExistingCachedResourcesAllowed, mScriptLoader)); + } + + // The worker may have a null principal on first load, but in that case its + // parent definitely will have one. + nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + + nsresult rv = mCacheCreator->Load(principal); + if (NS_WARN_IF(NS_FAILED(rv))) { + CancelMainThread(rv); + return rv; + } + + return NS_OK; +} + +nsresult ScriptLoaderRunnable::OnStreamComplete( + ThreadSafeRequestHandle* aRequestHandle, nsresult aStatus) { + AssertIsOnMainThread(); + + LoadingFinished(aRequestHandle, aStatus); + return NS_OK; +} + +void ScriptLoaderRunnable::LoadingFinished( + ThreadSafeRequestHandle* aRequestHandle, nsresult aRv) { + AssertIsOnMainThread(); + + WorkerLoadContext* loadContext = aRequestHandle->GetContext(); + + loadContext->mLoadResult = aRv; + MOZ_ASSERT(!loadContext->mLoadingFinished); + loadContext->mLoadingFinished = true; + + if (loadContext->IsTopLevel() && NS_SUCCEEDED(aRv)) { + MOZ_DIAGNOSTIC_ASSERT( + mWorkerRef->Private()->PrincipalURIMatchesScriptURL()); + } + + MaybeExecuteFinishedScripts(aRequestHandle); +} + +void ScriptLoaderRunnable::MaybeExecuteFinishedScripts( + ThreadSafeRequestHandle* aRequestHandle) { + AssertIsOnMainThread(); + + // We execute the last step if we don't have a pending operation with the + // cache and the loading is completed. + WorkerLoadContext* loadContext = aRequestHandle->GetContext(); + if (!loadContext->IsAwaitingPromise()) { + if (aRequestHandle->GetContext()->IsTopLevel()) { + mWorkerRef->Private()->WorkerScriptLoaded(); + } + DispatchProcessPendingRequests(); + } +} + +void ScriptLoaderRunnable::CancelMainThreadWithBindingAborted() { + AssertIsOnMainThread(); + CancelMainThread(NS_BINDING_ABORTED); +} + +void ScriptLoaderRunnable::CancelMainThread(nsresult aCancelResult) { + AssertIsOnMainThread(); + if (IsCancelled()) { + return; + } + + { + MutexAutoLock lock(mScriptLoader->CleanUpLock()); + + // Check if we have already cancelled, or if the worker has been killed + // before we cancel. + if (mScriptLoader->CleanedUp()) { + return; + } + + mCancelMainThread = Some(aCancelResult); + + for (ThreadSafeRequestHandle* handle : mLoadingRequests) { + if (handle->IsEmpty()) { + continue; + } + + bool callLoadingFinished = true; + + WorkerLoadContext* loadContext = handle->GetContext(); + if (!loadContext) { + continue; + } + + if (loadContext->IsAwaitingPromise()) { + MOZ_ASSERT(mWorkerRef->Private()->IsServiceWorker()); + loadContext->mCachePromise->MaybeReject(NS_BINDING_ABORTED); + loadContext->mCachePromise = nullptr; + callLoadingFinished = false; + } + if (loadContext->mChannel) { + if (NS_SUCCEEDED(loadContext->mChannel->Cancel(aCancelResult))) { + callLoadingFinished = false; + } else { + NS_WARNING("Failed to cancel channel!"); + } + } + if (callLoadingFinished && !loadContext->mLoadingFinished) { + LoadingFinished(handle, aCancelResult); + } + } + DispatchProcessPendingRequests(); + } +} + +void ScriptLoaderRunnable::DispatchProcessPendingRequests() { + AssertIsOnMainThread(); + + const auto begin = mLoadingRequests.begin(); + const auto end = mLoadingRequests.end(); + using Iterator = decltype(begin); + const auto maybeRangeToExecute = + [begin, end]() -> Maybe<std::pair<Iterator, Iterator>> { + // firstItToExecute is the first loadInfo where mExecutionScheduled is + // unset. + auto firstItToExecute = std::find_if( + begin, end, [](const RefPtr<ThreadSafeRequestHandle>& requestHandle) { + return !requestHandle->mExecutionScheduled; + }); + + if (firstItToExecute == end) { + return Nothing(); + } + + // firstItUnexecutable is the first loadInfo that is not yet finished. + // Update mExecutionScheduled on the ones we're about to schedule for + // execution. + const auto firstItUnexecutable = + std::find_if(firstItToExecute, end, + [](RefPtr<ThreadSafeRequestHandle>& requestHandle) { + MOZ_ASSERT(!requestHandle->IsEmpty()); + if (!requestHandle->Finished()) { + return true; + } + + // We can execute this one. + requestHandle->mExecutionScheduled = true; + + return false; + }); + + return firstItUnexecutable == firstItToExecute + ? Nothing() + : Some(std::pair(firstItToExecute, firstItUnexecutable)); + }(); + + // If there are no unexecutable load infos, we can unuse things before the + // execution of the scripts and the stopping of the sync loop. + if (maybeRangeToExecute) { + if (maybeRangeToExecute->second == end) { + mCacheCreator = nullptr; + } + + RefPtr<ScriptExecutorRunnable> runnable = new ScriptExecutorRunnable( + mScriptLoader, mWorkerRef->Private(), mScriptLoader->mSyncLoopTarget, + Span<RefPtr<ThreadSafeRequestHandle>>{maybeRangeToExecute->first, + maybeRangeToExecute->second}); + + if (!runnable->Dispatch() && mScriptLoader->mSyncLoopTarget) { + MOZ_ASSERT(false, "This should never fail!"); + } + } +} + +ScriptExecutorRunnable::ScriptExecutorRunnable( + WorkerScriptLoader* aScriptLoader, WorkerPrivate* aWorkerPrivate, + nsISerialEventTarget* aSyncLoopTarget, + Span<RefPtr<ThreadSafeRequestHandle>> aLoadedRequests) + : MainThreadWorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget, + "ScriptExecutorRunnable"), + mScriptLoader(aScriptLoader), + mLoadedRequests(aLoadedRequests) {} + +bool ScriptExecutorRunnable::IsDebuggerRunnable() const { + // ScriptExecutorRunnable is used to execute both worker and debugger scripts. + // In the latter case, the runnable needs to be dispatched to the debugger + // queue. + return mScriptLoader->IsDebuggerScript(); +} + +bool ScriptExecutorRunnable::PreRun(WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // We must be on the same worker as we started on. + MOZ_ASSERT( + mScriptLoader->mSyncLoopTarget == mSyncLoopTarget, + "Unexpected SyncLoopTarget. Check if the sync loop was closed early"); + + { + // There is a possibility that we cleaned up while this task was waiting to + // run. If this has happened, return and exit. + MutexAutoLock lock(mScriptLoader->CleanUpLock()); + if (mScriptLoader->CleanedUp()) { + return true; + } + + const auto& requestHandle = mLoadedRequests[0]; + // Check if the request is still valid. + if (requestHandle->IsEmpty() || + !requestHandle->GetContext()->IsTopLevel()) { + return true; + } + } + + return mScriptLoader->StoreCSP(); +} + +bool ScriptExecutorRunnable::ProcessModuleScript( + JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + // We should only ever have one script when processing modules + MOZ_ASSERT(mLoadedRequests.Length() == 1); + RefPtr<ScriptLoadRequest> request; + { + // There is a possibility that we cleaned up while this task was waiting to + // run. If this has happened, return and exit. + MutexAutoLock lock(mScriptLoader->CleanUpLock()); + if (mScriptLoader->CleanedUp()) { + return true; + } + + MOZ_ASSERT(mLoadedRequests.Length() == 1); + const auto& requestHandle = mLoadedRequests[0]; + // The request must be valid. + MOZ_ASSERT(!requestHandle->IsEmpty()); + + // Release the request to the worker. From this point on, the Request Handle + // is empty. + request = requestHandle->ReleaseRequest(); + + // release lock. We will need it later if we cleanup. + } + + MOZ_ASSERT(request->IsModuleRequest()); + + WorkerLoadContext* loadContext = request->GetWorkerLoadContext(); + ModuleLoadRequest* moduleRequest = request->AsModuleRequest(); + + // DecreaseLoadingModuleRequestCount must be called before OnFetchComplete. + // OnFetchComplete will call ProcessPendingRequests, and in + // ProcessPendingRequests it will try to shutdown if + // AllModuleRequestsLoaded() returns true. + mScriptLoader->DecreaseLoadingModuleRequestCount(); + moduleRequest->OnFetchComplete(loadContext->mLoadResult); + + if (NS_FAILED(loadContext->mLoadResult)) { + if (moduleRequest->IsDynamicImport()) { + if (request->isInList()) { + moduleRequest->CancelDynamicImport(loadContext->mLoadResult); + mScriptLoader->TryShutdown(); + } + } else if (!moduleRequest->IsTopLevel()) { + moduleRequest->Cancel(); + mScriptLoader->TryShutdown(); + } else { + moduleRequest->LoadFailed(); + } + } + return true; +} + +bool ScriptExecutorRunnable::ProcessClassicScripts( + JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + // There is a possibility that we cleaned up while this task was waiting to + // run. If this has happened, return and exit. + { + MutexAutoLock lock(mScriptLoader->CleanUpLock()); + if (mScriptLoader->CleanedUp()) { + return true; + } + + for (const auto& requestHandle : mLoadedRequests) { + // The request must be valid. + MOZ_ASSERT(!requestHandle->IsEmpty()); + + // Release the request to the worker. From this point on, the Request + // Handle is empty. + RefPtr<ScriptLoadRequest> request = requestHandle->ReleaseRequest(); + mScriptLoader->MaybeMoveToLoadedList(request); + } + } + return mScriptLoader->ProcessPendingRequests(aCx); +} + +bool ScriptExecutorRunnable::WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // We must be on the same worker as we started on. + MOZ_ASSERT( + mScriptLoader->mSyncLoopTarget == mSyncLoopTarget, + "Unexpected SyncLoopTarget. Check if the sync loop was closed early"); + + if (mLoadedRequests.begin()->get()->GetRequest()->IsModuleRequest()) { + return ProcessModuleScript(aCx, aWorkerPrivate); + } + + return ProcessClassicScripts(aCx, aWorkerPrivate); +} + +nsresult ScriptExecutorRunnable::Cancel() { + if (mScriptLoader->AllScriptsExecuted() && + mScriptLoader->AllModuleRequestsLoaded()) { + mScriptLoader->ShutdownScriptLoader(false, false); + } + return NS_OK; +} + +} /* namespace loader */ + +nsresult ChannelFromScriptURLMainThread( + nsIPrincipal* aPrincipal, Document* aParentDoc, nsILoadGroup* aLoadGroup, + nsIURI* aScriptURL, const WorkerType& aWorkerType, + const RequestCredentials& aCredentials, + const Maybe<ClientInfo>& aClientInfo, + nsContentPolicyType aMainScriptContentPolicyType, + nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo, + nsIChannel** aChannel) { + AssertIsOnMainThread(); + + nsCOMPtr<nsIIOService> ios(do_GetIOService()); + + nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(secMan, "This should never be null!"); + + uint32_t secFlags; + nsresult rv; + if (aWorkerType == WorkerType::Module) { + rv = GetModuleSecFlags(true, aPrincipal, WorkerScript, aScriptURL, + aCredentials, secFlags); + } else { + rv = GetClassicSecFlags(true, aScriptURL, aPrincipal, WorkerScript, + secFlags); + } + if (NS_FAILED(rv)) { + return rv; + } + + return ChannelFromScriptURL( + aPrincipal, aParentDoc, nullptr, aLoadGroup, ios, secMan, aScriptURL, + aClientInfo, Maybe<ServiceWorkerDescriptor>(), true, WorkerScript, + aMainScriptContentPolicyType, nsIRequest::LOAD_NORMAL, secFlags, + aCookieJarSettings, aReferrerInfo, aChannel); +} + +nsresult ChannelFromScriptURLWorkerThread( + JSContext* aCx, WorkerPrivate* aParent, const nsAString& aScriptURL, + const WorkerType& aWorkerType, const RequestCredentials& aCredentials, + WorkerLoadInfo& aLoadInfo) { + aParent->AssertIsOnWorkerThread(); + + RefPtr<ChannelGetterRunnable> getter = new ChannelGetterRunnable( + aParent, aScriptURL, aWorkerType, aCredentials, aLoadInfo); + + ErrorResult rv; + getter->Dispatch(Canceling, rv); + if (rv.Failed()) { + NS_ERROR("Failed to dispatch!"); + return rv.StealNSResult(); + } + + return getter->GetResult(); +} + +void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, + const nsAString& aScriptURL) { + MOZ_ASSERT(!aRv.Failed()); + + nsPrintfCString err("Failed to load worker script at \"%s\"", + NS_ConvertUTF16toUTF8(aScriptURL).get()); + + switch (aLoadResult) { + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_NOT_AVAILABLE: + case NS_ERROR_CORRUPTED_CONTENT: + aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); + break; + + case NS_ERROR_MALFORMED_URI: + case NS_ERROR_DOM_SYNTAX_ERR: + aRv.ThrowSyntaxError(err); + break; + + case NS_BINDING_ABORTED: + // Note: we used to pretend like we didn't set an exception for + // NS_BINDING_ABORTED, but then ShutdownScriptLoader did it anyway. The + // other callsite, in WorkerPrivate::Constructor, never passed in + // NS_BINDING_ABORTED. So just throw it directly here. Consumers will + // deal as needed. But note that we do NOT want to use one of the + // Throw*Error() methods on ErrorResult for this case, because that will + // make it impossible for consumers to realize that our error was + // NS_BINDING_ABORTED. + aRv.Throw(aLoadResult); + return; + + case NS_ERROR_DOM_BAD_URI: + // This is actually a security error. + case NS_ERROR_DOM_SECURITY_ERR: + aRv.ThrowSecurityError(err); + break; + + default: + // For lack of anything better, go ahead and throw a NetworkError here. + // We don't want to throw a JS exception, because for toplevel script + // loads that would get squelched. + aRv.ThrowNetworkError(nsPrintfCString( + "Failed to load worker script at %s (nsresult = 0x%" PRIx32 ")", + NS_ConvertUTF16toUTF8(aScriptURL).get(), + static_cast<uint32_t>(aLoadResult))); + return; + } +} + +void LoadMainScript(WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + const nsAString& aScriptURL, + WorkerScriptType aWorkerScriptType, ErrorResult& aRv, + const mozilla::Encoding* aDocumentEncoding) { + nsTArray<nsString> scriptURLs; + + scriptURLs.AppendElement(aScriptURL); + + LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), scriptURLs, true, + aWorkerScriptType, aRv, aDocumentEncoding); +} + +void Load(WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + const nsTArray<nsString>& aScriptURLs, + WorkerScriptType aWorkerScriptType, ErrorResult& aRv) { + const uint32_t urlCount = aScriptURLs.Length(); + + if (!urlCount) { + return; + } + + if (urlCount > MAX_CONCURRENT_SCRIPTS) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), aScriptURLs, false, + aWorkerScriptType, aRv); +} + +} // namespace mozilla::dom::workerinternals diff --git a/dom/workers/ScriptLoader.h b/dom/workers/ScriptLoader.h new file mode 100644 index 0000000000..010f3d8316 --- /dev/null +++ b/dom/workers/ScriptLoader.h @@ -0,0 +1,380 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_scriptloader_h__ +#define mozilla_dom_workers_scriptloader_h__ + +#include "js/loader/ScriptLoadRequest.h" +#include "js/loader/ModuleLoadRequest.h" +#include "js/loader/ModuleLoaderBase.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerLoadContext.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/workerinternals/WorkerModuleLoader.h" +#include "mozilla/Maybe.h" +#include "nsIContentPolicy.h" +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" + +class nsIChannel; +class nsICookieJarSettings; +class nsILoadGroup; +class nsIPrincipal; +class nsIReferrerInfo; +class nsIURI; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class ClientInfo; +class Document; +struct WorkerLoadInfo; +class WorkerPrivate; +class SerializedStackHolder; + +enum WorkerScriptType { WorkerScript, DebuggerScript }; + +namespace workerinternals { + +namespace loader { +class ScriptExecutorRunnable; +class ScriptLoaderRunnable; +class CachePromiseHandler; +class CacheLoadHandler; +class CacheCreator; +class NetworkLoadHandler; + +/* + * [DOMDOC] WorkerScriptLoader + * + * The WorkerScriptLoader is the primary class responsible for loading all + * Workers, including: ServiceWorkers, SharedWorkers, RemoteWorkers, and + * dedicated Workers. Our implementation also includes a subtype of dedicated + * workers: ChromeWorker, which exposes information that isn't normally + * accessible on a dedicated worker. See [1] for more information. + * + * Due to constraints around fetching, this class currently delegates the + * "Fetch" portion of its work load to the main thread. Unlike the DOM + * ScriptLoader, the WorkerScriptLoader is not persistent and is not reused for + * subsequent loads. That means for each iteration of loading (for example, + * loading the main script, followed by a load triggered by ImportScripts), we + * recreate this class, and handle the case independently. + * + * The flow of requests across the boundaries looks like this: + * + * +----------------------------+ + * | new WorkerScriptLoader(..) | + * +----------------------------+ + * | + * V + * +-------------------------------------------+ + * | WorkerScriptLoader::DispatchLoadScripts() | + * +-------------------------------------------+ + * | + * V + * +............................+ + * | new ScriptLoaderRunnable() | + * +............................+ + * : + * V + * ##################################################################### + * Enter Main thread + * ##################################################################### + * : + * V + * +.............................+ For each: Is a normal Worker? + * | ScriptLoaderRunnable::Run() |----------------------------------+ + * +.............................+ | + * | V + * | +----------------------------------+ + * | | WorkerScriptLoader::LoadScript() | + * | +----------------------------------+ + * | | + * | For each request: Is a ServiceWorker? | + * | | + * V V + * +==================+ No script in cache? +====================+ + * | CacheLoadHandler |------------------------>| NetworkLoadHandler | + * +==================+ +====================+ + * : : + * : Loaded from Cache : Loaded by Network + * : +..........................................+ : + * +---| ScriptLoaderRunnable::OnStreamComplete() |<----+ + * +..........................................+ + * | + * | A request is ready, is it in post order? + * | + * | call DispatchPendingProcessRequests() + * | This creates ScriptExecutorRunnable + * +..............................+ + * | new ScriptLoaderExecutable() | + * +..............................+ + * : + * V + * ##################################################################### + * Enter worker thread + * ##################################################################### + * : + * V + * +...............................+ All Scripts Executed? + * | ScriptLoaderExecutable::Run() | -------------+ + * +...............................+ : + * : + * : yes. Do execution + * : then shutdown. + * : + * V + * +--------------------------------------------+ + * | WorkerScriptLoader::ShutdownScriptLoader() | + * +--------------------------------------------+ + */ + +class WorkerScriptLoader : public JS::loader::ScriptLoaderInterface, + public nsINamed { + friend class ScriptExecutorRunnable; + friend class ScriptLoaderRunnable; + friend class CachePromiseHandler; + friend class CacheLoadHandler; + friend class CacheCreator; + friend class NetworkLoadHandler; + friend class WorkerModuleLoader; + + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + UniquePtr<SerializedStackHolder> mOriginStack; + nsString mOriginStackJSON; + nsCOMPtr<nsISerialEventTarget> mSyncLoopTarget; + ScriptLoadRequestList mLoadingRequests; + ScriptLoadRequestList mLoadedRequests; + Maybe<ServiceWorkerDescriptor> mController; + WorkerScriptType mWorkerScriptType; + ErrorResult& mRv; + bool mExecutionAborted = false; + bool mMutedErrorFlag = false; + + // Count of loading module requests. mLoadingRequests doesn't keep track of + // child module requests. + // This member should be accessed on worker thread. + uint32_t mLoadingModuleRequestCount; + + // Worker cancellation related Mutex + // + // Modified on the worker thread. + // It is ok to *read* this without a lock on the worker. + // Main thread must always acquire a lock. + bool mCleanedUp MOZ_GUARDED_BY( + mCleanUpLock); // To specify if the cleanUp() has been done. + + Mutex& CleanUpLock() MOZ_RETURN_CAPABILITY(mCleanUpLock) { + return mCleanUpLock; + } + + bool CleanedUp() const MOZ_REQUIRES(mCleanUpLock) { + mCleanUpLock.AssertCurrentThreadOwns(); + return mCleanedUp; + } + + // Ensure the worker and the main thread won't race to access |mCleanedUp|. + // Should be a MutexSingleWriter, but that causes a lot of issues when you + // expose the lock via Lock(). + Mutex mCleanUpLock; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + static already_AddRefed<WorkerScriptLoader> Create( + WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + nsISerialEventTarget* aSyncLoopTarget, WorkerScriptType aWorkerScriptType, + ErrorResult& aRv); + + bool CreateScriptRequests(const nsTArray<nsString>& aScriptURLs, + const mozilla::Encoding* aDocumentEncoding, + bool aIsMainScript); + + ScriptLoadRequest* GetMainScript(); + + already_AddRefed<ScriptLoadRequest> CreateScriptLoadRequest( + const nsString& aScriptURL, const mozilla::Encoding* aDocumentEncoding, + bool aIsMainScript); + + bool DispatchLoadScript(ScriptLoadRequest* aRequest); + + bool DispatchLoadScripts(); + + void TryShutdown(); + + WorkerScriptType GetWorkerScriptType() { return mWorkerScriptType; } + + protected: + nsIURI* GetBaseURI() const override; + + nsIURI* GetInitialBaseURI(); + + nsIGlobalObject* GetGlobal(); + + void MaybeMoveToLoadedList(ScriptLoadRequest* aRequest); + + bool StoreCSP(); + + bool ProcessPendingRequests(JSContext* aCx); + + bool AllScriptsExecuted() { + return mLoadingRequests.isEmpty() && mLoadedRequests.isEmpty(); + } + + bool IsDebuggerScript() const { return mWorkerScriptType == DebuggerScript; } + + void SetController(const Maybe<ServiceWorkerDescriptor>& aDescriptor) { + mController = aDescriptor; + } + + Maybe<ServiceWorkerDescriptor>& GetController() { return mController; } + + nsresult LoadScript(ThreadSafeRequestHandle* aRequestHandle); + + void ShutdownScriptLoader(bool aResult, bool aMutedError); + + private: + WorkerScriptLoader(UniquePtr<SerializedStackHolder> aOriginStack, + nsISerialEventTarget* aSyncLoopTarget, + WorkerScriptType aWorkerScriptType, ErrorResult& aRv); + + ~WorkerScriptLoader() = default; + + NS_IMETHOD + GetName(nsACString& aName) override { + aName.AssignLiteral("WorkerScriptLoader"); + return NS_OK; + } + + void InitModuleLoader(); + + nsTArray<RefPtr<ThreadSafeRequestHandle>> GetLoadingList(); + + bool IsDynamicImport(ScriptLoadRequest* aRequest); + + nsContentPolicyType GetContentPolicyType(ScriptLoadRequest* aRequest); + + bool EvaluateScript(JSContext* aCx, ScriptLoadRequest* aRequest); + + nsresult FillCompileOptionsForRequest( + JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle<JSScript*> aIntroductionScript) override; + + void ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const override; + + // Only used by import maps, crash if we get here. + void ReportWarningToConsole( + ScriptLoadRequest* aRequest, const char* aMessageName, + const nsTArray<nsString>& aParams = nsTArray<nsString>()) const override { + MOZ_CRASH("Import maps have not been implemented for this context"); + } + + void LogExceptionToConsole(JSContext* aCx, WorkerPrivate* aWorkerPrivate); + + bool AllModuleRequestsLoaded() const; + void IncreaseLoadingModuleRequestCount(); + void DecreaseLoadingModuleRequestCount(); +}; + +/* ScriptLoaderRunnable + * + * Responsibilities of this class: + * - the actual dispatch + * - delegating the load back to WorkerScriptLoader + * - handling the collections of scripts being requested + * - handling main thread cancellation + * - dispatching back to the worker thread + */ +class ScriptLoaderRunnable final : public nsIRunnable, public nsINamed { + RefPtr<WorkerScriptLoader> mScriptLoader; + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + nsTArrayView<RefPtr<ThreadSafeRequestHandle>> mLoadingRequests; + Maybe<nsresult> mCancelMainThread; + RefPtr<CacheCreator> mCacheCreator; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit ScriptLoaderRunnable( + WorkerScriptLoader* aScriptLoader, + nsTArray<RefPtr<ThreadSafeRequestHandle>> aLoadingRequests); + + nsresult OnStreamComplete(ThreadSafeRequestHandle* aRequestHandle, + nsresult aStatus); + + void LoadingFinished(ThreadSafeRequestHandle* aRequestHandle, nsresult aRv); + + void MaybeExecuteFinishedScripts(ThreadSafeRequestHandle* aRequestHandle); + + bool IsCancelled() { return mCancelMainThread.isSome(); } + + nsresult GetCancelResult() { + return (IsCancelled()) ? mCancelMainThread.ref() : NS_OK; + } + + void CancelMainThreadWithBindingAborted(); + + CacheCreator* GetCacheCreator() { return mCacheCreator; }; + + private: + ~ScriptLoaderRunnable() = default; + + void CancelMainThread(nsresult aCancelResult); + + void DispatchProcessPendingRequests(); + + NS_IMETHOD + Run() override; + + NS_IMETHOD + GetName(nsACString& aName) override { + aName.AssignLiteral("ScriptLoaderRunnable"); + return NS_OK; + } +}; + +} // namespace loader + +nsresult ChannelFromScriptURLMainThread( + nsIPrincipal* aPrincipal, Document* aParentDoc, nsILoadGroup* aLoadGroup, + nsIURI* aScriptURL, const WorkerType& aWorkerType, + const RequestCredentials& aCredentials, + const Maybe<ClientInfo>& aClientInfo, + nsContentPolicyType aContentPolicyType, + nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo, + nsIChannel** aChannel); + +nsresult ChannelFromScriptURLWorkerThread( + JSContext* aCx, WorkerPrivate* aParent, const nsAString& aScriptURL, + const WorkerType& aWorkerType, const RequestCredentials& aCredentials, + WorkerLoadInfo& aLoadInfo); + +void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, + const nsAString& aScriptURL); + +void LoadMainScript(WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + const nsAString& aScriptURL, + WorkerScriptType aWorkerScriptType, ErrorResult& aRv, + const mozilla::Encoding* aDocumentEncoding); + +void Load(WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + const nsTArray<nsString>& aScriptURLs, + WorkerScriptType aWorkerScriptType, ErrorResult& aRv); + +} // namespace workerinternals + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_workers_scriptloader_h__ */ diff --git a/dom/workers/Worker.cpp b/dom/workers/Worker.cpp new file mode 100644 index 0000000000..2348572e65 --- /dev/null +++ b/dom/workers/Worker.cpp @@ -0,0 +1,222 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Worker.h" + +#include "MessageEventRunnable.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "WorkerPrivate.h" +#include "EventWithOptionsRunnable.h" +#include "js/RootingAPI.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsISupports.h" +#include "nsDebug.h" +#include "mozilla/dom/WorkerStatus.h" +#include "mozilla/RefPtr.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +namespace mozilla::dom { + +/* static */ +already_AddRefed<Worker> Worker::Constructor(const GlobalObject& aGlobal, + const nsAString& aScriptURL, + const WorkerOptions& aOptions, + ErrorResult& aRv) { + JSContext* cx = aGlobal.Context(); + + nsCOMPtr<nsIGlobalObject> globalObject = + do_QueryInterface(aGlobal.GetAsSupports()); + + if (globalObject->GetAsInnerWindow() && + !globalObject->GetAsInnerWindow()->IsCurrentInnerWindow()) { + aRv.ThrowInvalidStateError( + "Cannot create worker for a going to be discarded document"); + return nullptr; + } + + RefPtr<WorkerPrivate> workerPrivate = WorkerPrivate::Constructor( + cx, aScriptURL, false /* aIsChromeWorker */, WorkerKindDedicated, + aOptions.mCredentials, aOptions.mType, aOptions.mName, VoidCString(), + nullptr /*aLoadInfo */, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<Worker> worker = new Worker(globalObject, workerPrivate.forget()); + return worker.forget(); +} + +Worker::Worker(nsIGlobalObject* aGlobalObject, + already_AddRefed<WorkerPrivate> aWorkerPrivate) + : DOMEventTargetHelper(aGlobalObject), + mWorkerPrivate(std::move(aWorkerPrivate)) { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->SetParentEventTargetRef(this); +} + +Worker::~Worker() { Terminate(); } + +JSObject* Worker::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + JS::Rooted<JSObject*> wrapper(aCx, + Worker_Binding::Wrap(aCx, this, aGivenProto)); + if (wrapper) { + // Most DOM objects don't assume they have a reflector. If they don't have + // one and need one, they create it. But in workers code, we assume that the + // reflector is always present. In order to guarantee that it's always + // present, we have to preserve it. Otherwise the GC will happily collect it + // as needed. + MOZ_ALWAYS_TRUE(TryPreserveWrapper(wrapper)); + } + + return wrapper; +} + +void Worker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, + ErrorResult& aRv) { + NS_ASSERT_OWNINGTHREAD(Worker); + + if (!mWorkerPrivate || mWorkerPrivate->ParentStatusProtected() > Running) { + return; + } + RefPtr<WorkerPrivate> workerPrivate = mWorkerPrivate; + Unused << workerPrivate; + + JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue()); + + aRv = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransferable, + &transferable); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + NS_ConvertUTF16toUTF8 nameOrScriptURL( + mWorkerPrivate->WorkerName().IsEmpty() + ? Substring( + mWorkerPrivate->ScriptURL(), 0, + std::min(size_t(1024), mWorkerPrivate->ScriptURL().Length())) + : Substring( + mWorkerPrivate->WorkerName(), 0, + std::min(size_t(1024), mWorkerPrivate->WorkerName().Length()))); + AUTO_PROFILER_MARKER_TEXT("Worker.postMessage", DOM, {}, nameOrScriptURL); + uint32_t flags = uint32_t(js::ProfilingStackFrame::Flags::RELEVANT_FOR_JS); + if (mWorkerPrivate->IsChromeWorker()) { + flags |= uint32_t(js::ProfilingStackFrame::Flags::NONSENSITIVE); + } + mozilla::AutoProfilerLabel PROFILER_RAII( + "Worker.postMessage", nameOrScriptURL.get(), + JS::ProfilingCategoryPair::DOM, flags); + + RefPtr<MessageEventRunnable> runnable = + new MessageEventRunnable(mWorkerPrivate, WorkerRunnable::WorkerThread); + + JS::CloneDataPolicy clonePolicy; + // DedicatedWorkers are always part of the same agent cluster. + clonePolicy.allowIntraClusterClonableSharedObjects(); + + if (NS_IsMainThread()) { + nsGlobalWindowInner* win = nsContentUtils::IncumbentInnerWindow(); + if (win && win->IsSharedMemoryAllowed()) { + clonePolicy.allowSharedMemoryObjects(); + } + } else { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + if (worker && worker->IsSharedMemoryAllowed()) { + clonePolicy.allowSharedMemoryObjects(); + } + } + + runnable->Write(aCx, aMessage, transferable, clonePolicy, aRv); + + if (!mWorkerPrivate || mWorkerPrivate->ParentStatusProtected() > Running) { + return; + } + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // The worker could have closed between the time we entered this function and + // checked ParentStatusProtected and now, which could cause the dispatch to + // fail. + Unused << NS_WARN_IF(!runnable->Dispatch()); +} + +void Worker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const StructuredSerializeOptions& aOptions, + ErrorResult& aRv) { + PostMessage(aCx, aMessage, aOptions.mTransfer, aRv); +} + +void Worker::PostEventWithOptions(JSContext* aCx, + JS::Handle<JS::Value> aOptions, + const Sequence<JSObject*>& aTransferable, + EventWithOptionsRunnable* aRunnable, + ErrorResult& aRv) { + NS_ASSERT_OWNINGTHREAD(Worker); + + if (NS_WARN_IF(!mWorkerPrivate || + mWorkerPrivate->ParentStatusProtected() > Running)) { + return; + } + RefPtr<WorkerPrivate> workerPrivate = mWorkerPrivate; + Unused << workerPrivate; + + aRunnable->InitOptions(aCx, aOptions, aTransferable, aRv); + + if (NS_WARN_IF(!mWorkerPrivate || + mWorkerPrivate->ParentStatusProtected() > Running)) { + return; + } + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + Unused << NS_WARN_IF(!aRunnable->Dispatch()); +} + +void Worker::Terminate() { + NS_ASSERT_OWNINGTHREAD(Worker); + + if (mWorkerPrivate) { + mWorkerPrivate->Cancel(); + mWorkerPrivate = nullptr; + } +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(Worker) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Worker, DOMEventTargetHelper) + if (tmp->mWorkerPrivate) { + tmp->mWorkerPrivate->Traverse(cb); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Worker, DOMEventTargetHelper) + tmp->Terminate(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Worker, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Worker) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(Worker, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(Worker, DOMEventTargetHelper) + +} // namespace mozilla::dom diff --git a/dom/workers/Worker.h b/dom/workers/Worker.h new file mode 100644 index 0000000000..14d0630f28 --- /dev/null +++ b/dom/workers/Worker.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_Worker_h +#define mozilla_dom_Worker_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/DebuggerNotificationBinding.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/RefPtr.h" +#include "mozilla/WeakPtr.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +namespace mozilla::dom { + +class EventWithOptionsRunnable; +struct StructuredSerializeOptions; +struct WorkerOptions; +class WorkerPrivate; + +class Worker : public DOMEventTargetHelper, public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(Worker, + DOMEventTargetHelper) + static already_AddRefed<Worker> Constructor(const GlobalObject& aGlobal, + const nsAString& aScriptURL, + const WorkerOptions& aOptions, + ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + Maybe<EventCallbackDebuggerNotificationType> GetDebuggerNotificationType() + const override { + return Some(EventCallbackDebuggerNotificationType::Worker); + } + + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv); + + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const StructuredSerializeOptions& aOptions, + ErrorResult& aRv); + + void PostEventWithOptions(JSContext* aCx, JS::Handle<JS::Value> aOptions, + const Sequence<JSObject*>& aTransferable, + EventWithOptionsRunnable* aRunnable, + ErrorResult& aRv); + + void Terminate(); + + IMPL_EVENT_HANDLER(error) + IMPL_EVENT_HANDLER(message) + IMPL_EVENT_HANDLER(messageerror) + + protected: + Worker(nsIGlobalObject* aGlobalObject, + already_AddRefed<WorkerPrivate> aWorkerPrivate); + ~Worker(); + + friend class EventWithOptionsRunnable; + RefPtr<WorkerPrivate> mWorkerPrivate; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_Worker_h */ diff --git a/dom/workers/WorkerCSPEventListener.cpp b/dom/workers/WorkerCSPEventListener.cpp new file mode 100644 index 0000000000..c693928486 --- /dev/null +++ b/dom/workers/WorkerCSPEventListener.cpp @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerCSPEventListener.h" +#include "WorkerRef.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "mozilla/dom/SecurityPolicyViolationEvent.h" +#include "mozilla/dom/SecurityPolicyViolationEventBinding.h" +#include "mozilla/dom/WorkerRunnable.h" + +using namespace mozilla::dom; + +namespace { + +class WorkerCSPEventRunnable final : public MainThreadWorkerRunnable { + public: + WorkerCSPEventRunnable(WorkerPrivate* aWorkerPrivate, const nsAString& aJSON) + : MainThreadWorkerRunnable(aWorkerPrivate, "WorkerCSPEventRunnable"), + mJSON(aJSON) {} + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + SecurityPolicyViolationEventInit violationEventInit; + if (NS_WARN_IF(!violationEventInit.Init(mJSON))) { + return true; + } + + RefPtr<mozilla::dom::Event> event = + mozilla::dom::SecurityPolicyViolationEvent::Constructor( + aWorkerPrivate->GlobalScope(), u"securitypolicyviolation"_ns, + violationEventInit); + event->SetTrusted(true); + + aWorkerPrivate->GlobalScope()->DispatchEvent(*event); + return true; + } + + const nsString mJSON; +}; + +} // namespace + +NS_IMPL_ISUPPORTS(WorkerCSPEventListener, nsICSPEventListener) + +/* static */ +already_AddRefed<WorkerCSPEventListener> WorkerCSPEventListener::Create( + WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<WorkerCSPEventListener> listener = new WorkerCSPEventListener(); + + MutexAutoLock lock(listener->mMutex); + listener->mWorkerRef = WeakWorkerRef::Create(aWorkerPrivate, [listener]() { + MutexAutoLock lock(listener->mMutex); + listener->mWorkerRef = nullptr; + }); + + if (NS_WARN_IF(!listener->mWorkerRef)) { + return nullptr; + } + + return listener.forget(); +} + +WorkerCSPEventListener::WorkerCSPEventListener() + : mMutex("WorkerCSPEventListener::mMutex") {} + +NS_IMETHODIMP +WorkerCSPEventListener::OnCSPViolationEvent(const nsAString& aJSON) { + MutexAutoLock lock(mMutex); + if (!mWorkerRef) { + return NS_OK; + } + + WorkerPrivate* workerPrivate = mWorkerRef->GetUnsafePrivate(); + MOZ_ASSERT(workerPrivate); + + if (NS_IsMainThread()) { + RefPtr<WorkerCSPEventRunnable> runnable = + new WorkerCSPEventRunnable(workerPrivate, aJSON); + runnable->Dispatch(); + + return NS_OK; + } + + SecurityPolicyViolationEventInit violationEventInit; + if (NS_WARN_IF(!violationEventInit.Init(aJSON))) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<mozilla::dom::Event> event = + mozilla::dom::SecurityPolicyViolationEvent::Constructor( + workerPrivate->GlobalScope(), u"securitypolicyviolation"_ns, + violationEventInit); + event->SetTrusted(true); + + workerPrivate->GlobalScope()->DispatchEvent(*event); + + return NS_OK; +} diff --git a/dom/workers/WorkerCSPEventListener.h b/dom/workers/WorkerCSPEventListener.h new file mode 100644 index 0000000000..1265f0ed17 --- /dev/null +++ b/dom/workers/WorkerCSPEventListener.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WorkerCSPEventListener_h +#define mozilla_dom_WorkerCSPEventListener_h + +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/Mutex.h" +#include "nsIContentSecurityPolicy.h" + +namespace mozilla::dom { + +class WeakWorkerRef; +class WorkerRef; +class WorkerPrivate; + +class WorkerCSPEventListener final : public nsICSPEventListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICSPEVENTLISTENER + + static already_AddRefed<WorkerCSPEventListener> Create( + WorkerPrivate* aWorkerPrivate); + + private: + WorkerCSPEventListener(); + ~WorkerCSPEventListener() = default; + + Mutex mMutex; + + // Protected by mutex. + RefPtr<WeakWorkerRef> mWorkerRef MOZ_GUARDED_BY(mMutex); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WorkerCSPEventListener_h diff --git a/dom/workers/WorkerChannelInfo.cpp b/dom/workers/WorkerChannelInfo.cpp new file mode 100644 index 0000000000..cea6acf1ef --- /dev/null +++ b/dom/workers/WorkerChannelInfo.cpp @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerChannelInfo.h" +#include "mozilla/dom/BrowsingContext.h" + +namespace mozilla::dom { + +// WorkerChannelLoadInfo + +NS_IMPL_ISUPPORTS(WorkerChannelLoadInfo, nsIWorkerChannelLoadInfo) + +NS_IMETHODIMP +WorkerChannelLoadInfo::GetWorkerAssociatedBrowsingContextID(uint64_t* aResult) { + *aResult = mWorkerAssociatedBrowsingContextID; + return NS_OK; +} + +NS_IMETHODIMP +WorkerChannelLoadInfo::SetWorkerAssociatedBrowsingContextID(uint64_t aID) { + mWorkerAssociatedBrowsingContextID = aID; + return NS_OK; +} + +NS_IMETHODIMP +WorkerChannelLoadInfo::GetWorkerAssociatedBrowsingContext( + dom::BrowsingContext** aResult) { + *aResult = BrowsingContext::Get(mWorkerAssociatedBrowsingContextID).take(); + return NS_OK; +} + +// WorkerChannelInfo + +NS_IMPL_ISUPPORTS(WorkerChannelInfo, nsIWorkerChannelInfo) + +WorkerChannelInfo::WorkerChannelInfo(uint64_t aChannelID, + uint64_t aBrowsingContextID) + : mChannelID(aChannelID) { + mLoadInfo = new WorkerChannelLoadInfo(); + mLoadInfo->SetWorkerAssociatedBrowsingContextID(aBrowsingContextID); +} + +NS_IMETHODIMP +WorkerChannelInfo::SetLoadInfo(nsIWorkerChannelLoadInfo* aLoadInfo) { + MOZ_ASSERT(aLoadInfo); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +WorkerChannelInfo::GetLoadInfo(nsIWorkerChannelLoadInfo** aLoadInfo) { + *aLoadInfo = do_AddRef(mLoadInfo).take(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerChannelInfo::GetChannelId(uint64_t* aChannelID) { + NS_ENSURE_ARG_POINTER(aChannelID); + *aChannelID = mChannelID; + return NS_OK; +} + +} // end of namespace mozilla::dom diff --git a/dom/workers/WorkerChannelInfo.h b/dom/workers/WorkerChannelInfo.h new file mode 100644 index 0000000000..66b28bf685 --- /dev/null +++ b/dom/workers/WorkerChannelInfo.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WorkerChannel_h +#define mozilla_dom_WorkerChannel_h + +#include "nsIWorkerChannelInfo.h" +#include "nsILoadInfo.h" +#include "nsIChannel.h" +#include "nsTArray.h" +#include "nsIURI.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/OriginAttributes.h" + +namespace mozilla::dom { + +class WorkerChannelLoadInfo final : public nsIWorkerChannelLoadInfo { + public: + NS_DECL_THREADSAFE_ISUPPORTS; + NS_DECL_NSIWORKERCHANNELLOADINFO; + + private: + ~WorkerChannelLoadInfo() = default; + + uint64_t mWorkerAssociatedBrowsingContextID; +}; + +class WorkerChannelInfo final : public nsIWorkerChannelInfo { + public: + NS_DECL_THREADSAFE_ISUPPORTS; + NS_DECL_NSIWORKERCHANNELINFO; + + WorkerChannelInfo(uint64_t aChannelID, + uint64_t aWorkerAssociatedBrowsingContextID); + WorkerChannelInfo() = delete; + + private: + ~WorkerChannelInfo() = default; + + nsCOMPtr<nsIWorkerChannelLoadInfo> mLoadInfo; + uint64_t mChannelID; +}; + +} // end of namespace mozilla::dom + +#endif diff --git a/dom/workers/WorkerCommon.h b/dom/workers/WorkerCommon.h new file mode 100644 index 0000000000..d10dabb5c5 --- /dev/null +++ b/dom/workers/WorkerCommon.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerCommon_h +#define mozilla_dom_workers_WorkerCommon_h + +#include "js/TypeDecls.h" + +class nsPIDOMWindowInner; + +namespace mozilla::dom { + +class WorkerPrivate; + +// All of these are implemented in RuntimeService.cpp + +WorkerPrivate* GetWorkerPrivateFromContext(JSContext* aCx); + +WorkerPrivate* GetCurrentThreadWorkerPrivate(); + +bool IsCurrentThreadRunningWorker(); + +bool IsCurrentThreadRunningChromeWorker(); + +JSContext* GetCurrentWorkerThreadJSContext(); + +JSObject* GetCurrentThreadWorkerGlobal(); + +JSObject* GetCurrentThreadWorkerDebuggerGlobal(); + +void CancelWorkersForWindow(const nsPIDOMWindowInner& aWindow); + +void FreezeWorkersForWindow(const nsPIDOMWindowInner& aWindow); + +void ThawWorkersForWindow(const nsPIDOMWindowInner& aWindow); + +void SuspendWorkersForWindow(const nsPIDOMWindowInner& aWindow); + +void ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow); + +void PropagateStorageAccessPermissionGrantedToWorkers( + const nsPIDOMWindowInner& aWindow); + +// All of these are implemented in WorkerScope.cpp + +bool IsWorkerGlobal(JSObject* global); + +bool IsWorkerDebuggerGlobal(JSObject* global); + +bool IsWorkerDebuggerSandbox(JSObject* object); + +} // namespace mozilla::dom + +#endif // mozilla_dom_workers_WorkerCommon_h diff --git a/dom/workers/WorkerDebugger.cpp b/dom/workers/WorkerDebugger.cpp new file mode 100644 index 0000000000..a3e0af7a38 --- /dev/null +++ b/dom/workers/WorkerDebugger.cpp @@ -0,0 +1,501 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/Encoding.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsThreadUtils.h" +#include "ScriptLoader.h" +#include "WorkerCommon.h" +#include "WorkerError.h" +#include "WorkerRunnable.h" +#include "WorkerDebugger.h" + +#if defined(XP_WIN) +# include <processthreadsapi.h> // for GetCurrentProcessId() +#else +# include <unistd.h> // for getpid() +#endif // defined(XP_WIN) + +namespace mozilla::dom { + +namespace { + +class DebuggerMessageEventRunnable final : public WorkerDebuggerRunnable { + nsString mMessage; + + public: + DebuggerMessageEventRunnable(WorkerPrivate* aWorkerPrivate, + const nsAString& aMessage) + : WorkerDebuggerRunnable(aWorkerPrivate, "DebuggerMessageEventRunnable"), + mMessage(aMessage) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + WorkerDebuggerGlobalScope* globalScope = + aWorkerPrivate->DebuggerGlobalScope(); + MOZ_ASSERT(globalScope); + + JS::Rooted<JSString*> message( + aCx, JS_NewUCStringCopyN(aCx, mMessage.get(), mMessage.Length())); + if (!message) { + return false; + } + JS::Rooted<JS::Value> data(aCx, JS::StringValue(message)); + + RefPtr<MessageEvent> event = + new MessageEvent(globalScope, nullptr, nullptr); + event->InitMessageEvent(nullptr, u"message"_ns, CanBubble::eNo, + Cancelable::eYes, data, u""_ns, u""_ns, nullptr, + Sequence<OwningNonNull<MessagePort>>()); + event->SetTrusted(true); + + globalScope->DispatchEvent(*event); + return true; + } +}; + +class CompileDebuggerScriptRunnable final : public WorkerDebuggerRunnable { + nsString mScriptURL; + const mozilla::Encoding* mDocumentEncoding; + + public: + CompileDebuggerScriptRunnable(WorkerPrivate* aWorkerPrivate, + const nsAString& aScriptURL, + const mozilla::Encoding* aDocumentEncoding) + : WorkerDebuggerRunnable(aWorkerPrivate, "CompileDebuggerScriptRunnable"), + mScriptURL(aScriptURL), + mDocumentEncoding(aDocumentEncoding) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + + WorkerDebuggerGlobalScope* globalScope = + aWorkerPrivate->CreateDebuggerGlobalScope(aCx); + if (!globalScope) { + NS_WARNING("Failed to make global!"); + return false; + } + + if (NS_WARN_IF(!aWorkerPrivate->EnsureCSPEventListener())) { + return false; + } + + JS::Rooted<JSObject*> global(aCx, globalScope->GetWrapper()); + + ErrorResult rv; + JSAutoRealm ar(aCx, global); + workerinternals::LoadMainScript(aWorkerPrivate, nullptr, mScriptURL, + DebuggerScript, rv, mDocumentEncoding); + rv.WouldReportJSException(); + // Explicitly ignore NS_BINDING_ABORTED on rv. Or more precisely, still + // return false and don't SetWorkerScriptExecutedSuccessfully() in that + // case, but don't throw anything on aCx. The idea is to not dispatch error + // events if our load is canceled with that error code. + if (rv.ErrorCodeIs(NS_BINDING_ABORTED)) { + rv.SuppressException(); + return false; + } + // Make sure to propagate exceptions from rv onto aCx, so that they will get + // reported after we return. We do this for all failures on rv, because now + // we're using rv to track all the state we care about. + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + return true; + } +}; + +} // namespace + +class WorkerDebugger::PostDebuggerMessageRunnable final : public Runnable { + WorkerDebugger* mDebugger; + nsString mMessage; + + public: + PostDebuggerMessageRunnable(WorkerDebugger* aDebugger, + const nsAString& aMessage) + : mozilla::Runnable("PostDebuggerMessageRunnable"), + mDebugger(aDebugger), + mMessage(aMessage) {} + + private: + ~PostDebuggerMessageRunnable() = default; + + NS_IMETHOD + Run() override { + mDebugger->PostMessageToDebuggerOnMainThread(mMessage); + + return NS_OK; + } +}; + +class WorkerDebugger::ReportDebuggerErrorRunnable final : public Runnable { + WorkerDebugger* mDebugger; + nsString mFilename; + uint32_t mLineno; + nsString mMessage; + + public: + ReportDebuggerErrorRunnable(WorkerDebugger* aDebugger, + const nsAString& aFilename, uint32_t aLineno, + const nsAString& aMessage) + : Runnable("ReportDebuggerErrorRunnable"), + mDebugger(aDebugger), + mFilename(aFilename), + mLineno(aLineno), + mMessage(aMessage) {} + + private: + ~ReportDebuggerErrorRunnable() = default; + + NS_IMETHOD + Run() override { + mDebugger->ReportErrorToDebuggerOnMainThread(mFilename, mLineno, mMessage); + + return NS_OK; + } +}; + +WorkerDebugger::WorkerDebugger(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate), mIsInitialized(false) { + AssertIsOnMainThread(); +} + +WorkerDebugger::~WorkerDebugger() { + MOZ_ASSERT(!mWorkerPrivate); + + if (!NS_IsMainThread()) { + for (auto& listener : mListeners) { + NS_ReleaseOnMainThread("WorkerDebugger::mListeners", listener.forget()); + } + } +} + +NS_IMPL_ISUPPORTS(WorkerDebugger, nsIWorkerDebugger) + +NS_IMETHODIMP +WorkerDebugger::GetIsClosed(bool* aResult) { + AssertIsOnMainThread(); + + *aResult = !mWorkerPrivate; + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetIsChrome(bool* aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mWorkerPrivate->IsChromeWorker(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetIsInitialized(bool* aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mIsInitialized; + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetParent(nsIWorkerDebugger** aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + WorkerPrivate* parent = mWorkerPrivate->GetParent(); + if (!parent) { + *aResult = nullptr; + return NS_OK; + } + + MOZ_ASSERT(mWorkerPrivate->IsDedicatedWorker()); + + nsCOMPtr<nsIWorkerDebugger> debugger = parent->Debugger(); + debugger.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetType(uint32_t* aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mWorkerPrivate->Kind(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetUrl(nsAString& aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + aResult = mWorkerPrivate->ScriptURL(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetWindow(mozIDOMWindow** aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsPIDOMWindowInner> window = DedicatedWorkerWindow(); + window.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetWindowIDs(nsTArray<uint64_t>& aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + if (mWorkerPrivate->IsDedicatedWorker()) { + if (const auto window = DedicatedWorkerWindow()) { + aResult.AppendElement(window->WindowID()); + } + } else if (mWorkerPrivate->IsSharedWorker()) { + const RemoteWorkerChild* const controller = + mWorkerPrivate->GetRemoteWorkerController(); + MOZ_ASSERT(controller); + aResult = controller->WindowIDs().Clone(); + } + + return NS_OK; +} + +nsCOMPtr<nsPIDOMWindowInner> WorkerDebugger::DedicatedWorkerWindow() { + MOZ_ASSERT(mWorkerPrivate); + + WorkerPrivate* worker = mWorkerPrivate; + while (worker->GetParent()) { + worker = worker->GetParent(); + } + + if (!worker->IsDedicatedWorker()) { + return nullptr; + } + + return worker->GetWindow(); +} + +NS_IMETHODIMP +WorkerDebugger::GetPrincipal(nsIPrincipal** aResult) { + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIPrincipal> prin = mWorkerPrivate->GetPrincipal(); + prin.forget(aResult); + + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetServiceWorkerID(uint32_t* aResult) { + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + if (!mWorkerPrivate || !mWorkerPrivate->IsServiceWorker()) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mWorkerPrivate->ServiceWorkerID(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetId(nsAString& aResult) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + aResult = mWorkerPrivate->Id(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::Initialize(const nsAString& aURL) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + // This should be non-null for dedicated workers and null for Shared and + // Service workers. All Encoding values are static and will live as long + // as the process and the convention is to therefore use raw pointers. + const mozilla::Encoding* aDocumentEncoding = + NS_IsMainThread() && !mWorkerPrivate->GetParent() && + mWorkerPrivate->GetDocument() + ? mWorkerPrivate->GetDocument()->GetDocumentCharacterSet().get() + : nullptr; + + if (!mIsInitialized) { + RefPtr<CompileDebuggerScriptRunnable> runnable = + new CompileDebuggerScriptRunnable(mWorkerPrivate, aURL, + aDocumentEncoding); + if (!runnable->Dispatch()) { + return NS_ERROR_FAILURE; + } + + mIsInitialized = true; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::PostMessageMoz(const nsAString& aMessage) { + AssertIsOnMainThread(); + + if (!mWorkerPrivate || !mIsInitialized) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<DebuggerMessageEventRunnable> runnable = + new DebuggerMessageEventRunnable(mWorkerPrivate, aMessage); + if (!runnable->Dispatch()) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::AddListener(nsIWorkerDebuggerListener* aListener) { + AssertIsOnMainThread(); + + if (mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::RemoveListener(nsIWorkerDebuggerListener* aListener) { + AssertIsOnMainThread(); + + if (!mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.RemoveElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::SetDebuggerReady(bool aReady) { + return mWorkerPrivate->SetIsDebuggerReady(aReady); +} + +void WorkerDebugger::Close() { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate = nullptr; + + for (const auto& listener : mListeners.Clone()) { + listener->OnClose(); + } +} + +void WorkerDebugger::PostMessageToDebugger(const nsAString& aMessage) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<PostDebuggerMessageRunnable> runnable = + new PostDebuggerMessageRunnable(this, aMessage); + if (NS_FAILED(mWorkerPrivate->DispatchToMainThreadForMessaging( + runnable.forget()))) { + NS_WARNING("Failed to post message to debugger on main thread!"); + } +} + +void WorkerDebugger::PostMessageToDebuggerOnMainThread( + const nsAString& aMessage) { + AssertIsOnMainThread(); + + for (const auto& listener : mListeners.Clone()) { + listener->OnMessage(aMessage); + } +} + +void WorkerDebugger::ReportErrorToDebugger(const nsAString& aFilename, + uint32_t aLineno, + const nsAString& aMessage) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<ReportDebuggerErrorRunnable> runnable = + new ReportDebuggerErrorRunnable(this, aFilename, aLineno, aMessage); + if (NS_FAILED(mWorkerPrivate->DispatchToMainThreadForMessaging( + runnable.forget()))) { + NS_WARNING("Failed to report error to debugger on main thread!"); + } +} + +void WorkerDebugger::ReportErrorToDebuggerOnMainThread( + const nsAString& aFilename, uint32_t aLineno, const nsAString& aMessage) { + AssertIsOnMainThread(); + + for (const auto& listener : mListeners.Clone()) { + listener->OnError(aFilename, aLineno, aMessage); + } + + AutoJSAPI jsapi; + // We're only using this context to deserialize a stack to report to the + // console, so the scope we use doesn't matter. Stack frame filtering happens + // based on the principal encoded into the frame and the caller compartment, + // not the compartment of the frame object, and the console reporting code + // will not be using our context, and therefore will not care what compartment + // it has entered. + DebugOnly<bool> ok = jsapi.Init(xpc::PrivilegedJunkScope()); + MOZ_ASSERT(ok, "PrivilegedJunkScope should exist"); + + WorkerErrorReport report; + report.mMessage = aMessage; + report.mFilename = aFilename; + WorkerErrorReport::LogErrorToConsole(jsapi.cx(), report, 0); +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerDebugger.h b/dom/workers/WorkerDebugger.h new file mode 100644 index 0000000000..3be7ef4203 --- /dev/null +++ b/dom/workers/WorkerDebugger.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerDebugger_h +#define mozilla_dom_workers_WorkerDebugger_h + +#include "mozilla/dom/WorkerScope.h" +#include "nsCOMPtr.h" +#include "nsIWorkerDebugger.h" + +class mozIDOMWindow; +class nsIPrincipal; +class nsPIDOMWindowInner; + +namespace mozilla::dom { + +class WorkerPrivate; + +class WorkerDebugger : public nsIWorkerDebugger { + class ReportDebuggerErrorRunnable; + class PostDebuggerMessageRunnable; + + CheckedUnsafePtr<WorkerPrivate> mWorkerPrivate; + bool mIsInitialized; + nsTArray<nsCOMPtr<nsIWorkerDebuggerListener>> mListeners; + + public: + explicit WorkerDebugger(WorkerPrivate* aWorkerPrivate); + + NS_DECL_ISUPPORTS + NS_DECL_NSIWORKERDEBUGGER + + void AssertIsOnParentThread(); + + void Close(); + + void PostMessageToDebugger(const nsAString& aMessage); + + void ReportErrorToDebugger(const nsAString& aFilename, uint32_t aLineno, + const nsAString& aMessage); + + private: + virtual ~WorkerDebugger(); + + void PostMessageToDebuggerOnMainThread(const nsAString& aMessage); + + void ReportErrorToDebuggerOnMainThread(const nsAString& aFilename, + uint32_t aLineno, + const nsAString& aMessage); + + nsCOMPtr<nsPIDOMWindowInner> DedicatedWorkerWindow(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_workers_WorkerDebugger_h diff --git a/dom/workers/WorkerDebuggerManager.cpp b/dom/workers/WorkerDebuggerManager.cpp new file mode 100644 index 0000000000..dfca4748b7 --- /dev/null +++ b/dom/workers/WorkerDebuggerManager.cpp @@ -0,0 +1,330 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerDebuggerManager.h" + +#include "nsSimpleEnumerator.h" + +#include "mozilla/dom/JSExecutionManager.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" + +#include "WorkerDebugger.h" +#include "WorkerPrivate.h" +#include "nsIObserverService.h" + +namespace mozilla::dom { + +namespace { + +class RegisterDebuggerMainThreadRunnable final : public mozilla::Runnable { + WorkerPrivate* mWorkerPrivate; + bool mNotifyListeners; + + public: + RegisterDebuggerMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + bool aNotifyListeners) + : mozilla::Runnable("RegisterDebuggerMainThreadRunnable"), + mWorkerPrivate(aWorkerPrivate), + mNotifyListeners(aNotifyListeners) {} + + private: + ~RegisterDebuggerMainThreadRunnable() = default; + + NS_IMETHOD + Run() override { + WorkerDebuggerManager* manager = WorkerDebuggerManager::Get(); + MOZ_ASSERT(manager); + + manager->RegisterDebuggerMainThread(mWorkerPrivate, mNotifyListeners); + return NS_OK; + } +}; + +class UnregisterDebuggerMainThreadRunnable final : public mozilla::Runnable { + WorkerPrivate* mWorkerPrivate; + + public: + explicit UnregisterDebuggerMainThreadRunnable(WorkerPrivate* aWorkerPrivate) + : mozilla::Runnable("UnregisterDebuggerMainThreadRunnable"), + mWorkerPrivate(aWorkerPrivate) {} + + private: + ~UnregisterDebuggerMainThreadRunnable() = default; + + NS_IMETHOD + Run() override { + WorkerDebuggerManager* manager = WorkerDebuggerManager::Get(); + MOZ_ASSERT(manager); + + manager->UnregisterDebuggerMainThread(mWorkerPrivate); + return NS_OK; + } +}; + +static StaticRefPtr<WorkerDebuggerManager> gWorkerDebuggerManager; + +} /* anonymous namespace */ + +class WorkerDebuggerEnumerator final : public nsSimpleEnumerator { + nsTArray<RefPtr<WorkerDebugger>> mDebuggers; + uint32_t mIndex; + + public: + explicit WorkerDebuggerEnumerator( + const nsTArray<RefPtr<WorkerDebugger>>& aDebuggers) + : mDebuggers(aDebuggers.Clone()), mIndex(0) {} + + NS_DECL_NSISIMPLEENUMERATOR + + const nsID& DefaultInterface() override { + return NS_GET_IID(nsIWorkerDebugger); + } + + private: + ~WorkerDebuggerEnumerator() override = default; +}; + +NS_IMETHODIMP +WorkerDebuggerEnumerator::HasMoreElements(bool* aResult) { + *aResult = mIndex < mDebuggers.Length(); + return NS_OK; +}; + +NS_IMETHODIMP +WorkerDebuggerEnumerator::GetNext(nsISupports** aResult) { + if (mIndex == mDebuggers.Length()) { + return NS_ERROR_FAILURE; + } + + mDebuggers.ElementAt(mIndex++).forget(aResult); + return NS_OK; +}; + +WorkerDebuggerManager::WorkerDebuggerManager() + : mMutex("WorkerDebuggerManager::mMutex") { + AssertIsOnMainThread(); +} + +WorkerDebuggerManager::~WorkerDebuggerManager() { AssertIsOnMainThread(); } + +// static +already_AddRefed<WorkerDebuggerManager> WorkerDebuggerManager::GetInstance() { + RefPtr<WorkerDebuggerManager> manager = WorkerDebuggerManager::GetOrCreate(); + return manager.forget(); +} + +// static +WorkerDebuggerManager* WorkerDebuggerManager::GetOrCreate() { + AssertIsOnMainThread(); + + if (!gWorkerDebuggerManager) { + // The observer service now owns us until shutdown. + gWorkerDebuggerManager = new WorkerDebuggerManager(); + if (NS_SUCCEEDED(gWorkerDebuggerManager->Init())) { + ClearOnShutdown(&gWorkerDebuggerManager); + } else { + NS_WARNING("Failed to initialize worker debugger manager!"); + gWorkerDebuggerManager = nullptr; + } + } + + return gWorkerDebuggerManager; +} + +WorkerDebuggerManager* WorkerDebuggerManager::Get() { + MOZ_ASSERT(gWorkerDebuggerManager); + return gWorkerDebuggerManager; +} + +NS_IMPL_ISUPPORTS(WorkerDebuggerManager, nsIObserver, nsIWorkerDebuggerManager); + +NS_IMETHODIMP +WorkerDebuggerManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Shutdown(); + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("Unknown observer topic!"); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebuggerManager::GetWorkerDebuggerEnumerator( + nsISimpleEnumerator** aResult) { + AssertIsOnMainThread(); + + RefPtr<WorkerDebuggerEnumerator> enumerator = + new WorkerDebuggerEnumerator(mDebuggers); + enumerator.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebuggerManager::AddListener( + nsIWorkerDebuggerManagerListener* aListener) { + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + if (mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebuggerManager::RemoveListener( + nsIWorkerDebuggerManagerListener* aListener) { + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + if (!mListeners.Contains(aListener)) { + return NS_OK; + } + + mListeners.RemoveElement(aListener); + return NS_OK; +} + +nsresult WorkerDebuggerManager::Init() { + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); + + nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void WorkerDebuggerManager::Shutdown() { + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + mListeners.Clear(); +} + +void WorkerDebuggerManager::RegisterDebugger(WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnParentThread(); + + if (NS_IsMainThread()) { + // When the parent thread is the main thread, it will always block until all + // register liseners have been called, since it cannot continue until the + // call to RegisterDebuggerMainThread returns. + // + // In this case, it is always safe to notify all listeners on the main + // thread, even if there were no listeners at the time this method was + // called, so we can always pass true for the value of aNotifyListeners. + // This avoids having to lock mMutex to check whether mListeners is empty. + RegisterDebuggerMainThread(aWorkerPrivate, true); + } else { + // We guarantee that if any register listeners are called, the worker does + // not start running until all register listeners have been called. To + // guarantee this, the parent thread should block until all register + // listeners have been called. + // + // However, to avoid overhead when the debugger is not being used, the + // parent thread will only block if there were any listeners at the time + // this method was called. As a result, we should not notify any listeners + // on the main thread if there were no listeners at the time this method was + // called, because the parent will not be blocking in that case. + bool hasListeners = false; + { + MutexAutoLock lock(mMutex); + + hasListeners = !mListeners.IsEmpty(); + } + + nsCOMPtr<nsIRunnable> runnable = + new RegisterDebuggerMainThreadRunnable(aWorkerPrivate, hasListeners); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL)); + + if (hasListeners) { + aWorkerPrivate->WaitForIsDebuggerRegistered(true); + } + } +} + +void WorkerDebuggerManager::UnregisterDebugger(WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnParentThread(); + + if (NS_IsMainThread()) { + UnregisterDebuggerMainThread(aWorkerPrivate); + } else { + nsCOMPtr<nsIRunnable> runnable = + new UnregisterDebuggerMainThreadRunnable(aWorkerPrivate); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL)); + + aWorkerPrivate->WaitForIsDebuggerRegistered(false); + } +} + +void WorkerDebuggerManager::RegisterDebuggerMainThread( + WorkerPrivate* aWorkerPrivate, bool aNotifyListeners) { + AssertIsOnMainThread(); + + RefPtr<WorkerDebugger> debugger = new WorkerDebugger(aWorkerPrivate); + mDebuggers.AppendElement(debugger); + + aWorkerPrivate->SetDebugger(debugger); + + if (aNotifyListeners) { + for (const auto& listener : CloneListeners()) { + listener->OnRegister(debugger); + } + } + + aWorkerPrivate->SetIsDebuggerRegistered(true); +} + +void WorkerDebuggerManager::UnregisterDebuggerMainThread( + WorkerPrivate* aWorkerPrivate) { + AssertIsOnMainThread(); + + // There is nothing to do here if the debugger was never succesfully + // registered. We need to check this on the main thread because the worker + // does not wait for the registration to complete if there were no listeners + // installed when it started. + if (!aWorkerPrivate->IsDebuggerRegistered()) { + return; + } + + RefPtr<WorkerDebugger> debugger = aWorkerPrivate->Debugger(); + mDebuggers.RemoveElement(debugger); + + aWorkerPrivate->SetDebugger(nullptr); + + for (const auto& listener : CloneListeners()) { + listener->OnUnregister(debugger); + } + + debugger->Close(); + aWorkerPrivate->SetIsDebuggerRegistered(false); +} + +uint32_t WorkerDebuggerManager::GetDebuggersLength() const { + return mDebuggers.Length(); +} + +WorkerDebugger* WorkerDebuggerManager::GetDebuggerAt(uint32_t aIndex) const { + return mDebuggers.SafeElementAt(aIndex, nullptr); +} + +nsTArray<nsCOMPtr<nsIWorkerDebuggerManagerListener>> +WorkerDebuggerManager::CloneListeners() { + MutexAutoLock lock(mMutex); + + return mListeners.Clone(); +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerDebuggerManager.h b/dom/workers/WorkerDebuggerManager.h new file mode 100644 index 0000000000..7c22a6b62c --- /dev/null +++ b/dom/workers/WorkerDebuggerManager.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_workerdebuggermanager_h +#define mozilla_dom_workers_workerdebuggermanager_h + +#include "MainThreadUtils.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Mutex.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsIObserver.h" +#include "nsISupports.h" +#include "nsIWorkerDebuggerManager.h" +#include "nsTArray.h" + +#define WORKERDEBUGGERMANAGER_CID \ + { \ + 0x62ec8731, 0x55ad, 0x4246, { \ + 0xb2, 0xea, 0xf2, 0x6c, 0x1f, 0xe1, 0x9d, 0x2d \ + } \ + } +#define WORKERDEBUGGERMANAGER_CONTRACTID \ + "@mozilla.org/dom/workers/workerdebuggermanager;1" + +namespace mozilla::dom { + +class WorkerDebugger; +class WorkerPrivate; + +class WorkerDebuggerManager final : public nsIObserver, + public nsIWorkerDebuggerManager { + Mutex mMutex MOZ_UNANNOTATED; + + // Protected by mMutex. + nsTArray<nsCOMPtr<nsIWorkerDebuggerManagerListener>> mListeners; + + // Only touched on the main thread. + nsTArray<RefPtr<WorkerDebugger>> mDebuggers; + + public: + static already_AddRefed<WorkerDebuggerManager> GetInstance(); + + static WorkerDebuggerManager* GetOrCreate(); + + static WorkerDebuggerManager* Get(); + + WorkerDebuggerManager(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIWORKERDEBUGGERMANAGER + + nsresult Init(); + + void Shutdown(); + + void RegisterDebugger(WorkerPrivate* aWorkerPrivate); + + void RegisterDebuggerMainThread(WorkerPrivate* aWorkerPrivate, + bool aNotifyListeners); + + void UnregisterDebugger(WorkerPrivate* aWorkerPrivate); + + void UnregisterDebuggerMainThread(WorkerPrivate* aWorkerPrivate); + + uint32_t GetDebuggersLength() const; + + WorkerDebugger* GetDebuggerAt(uint32_t aIndex) const; + + private: + nsTArray<nsCOMPtr<nsIWorkerDebuggerManagerListener>> CloneListeners(); + + virtual ~WorkerDebuggerManager(); +}; + +inline nsresult RegisterWorkerDebugger(WorkerPrivate* aWorkerPrivate) { + WorkerDebuggerManager* manager; + + if (NS_IsMainThread()) { + manager = WorkerDebuggerManager::GetOrCreate(); + if (!manager) { + NS_WARNING("Failed to create worker debugger manager!"); + return NS_ERROR_FAILURE; + } + } else { + manager = WorkerDebuggerManager::Get(); + } + + manager->RegisterDebugger(aWorkerPrivate); + return NS_OK; +} + +inline nsresult UnregisterWorkerDebugger(WorkerPrivate* aWorkerPrivate) { + WorkerDebuggerManager* manager; + + if (NS_IsMainThread()) { + manager = WorkerDebuggerManager::GetOrCreate(); + if (!manager) { + NS_WARNING("Failed to create worker debugger manager!"); + return NS_ERROR_FAILURE; + } + } else { + manager = WorkerDebuggerManager::Get(); + } + + manager->UnregisterDebugger(aWorkerPrivate); + return NS_OK; +} + +} // namespace mozilla::dom + +#endif // mozilla_dom_workers_workerdebuggermanager_h diff --git a/dom/workers/WorkerDocumentListener.cpp b/dom/workers/WorkerDocumentListener.cpp new file mode 100644 index 0000000000..cdbe30e072 --- /dev/null +++ b/dom/workers/WorkerDocumentListener.cpp @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerDocumentListener.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "nsGlobalWindowInner.h" + +namespace mozilla::dom { + +WorkerDocumentListener::WorkerDocumentListener() + : mMutex("mozilla::dom::WorkerDocumentListener::mMutex") {} + +WorkerDocumentListener::~WorkerDocumentListener() = default; + +RefPtr<WorkerDocumentListener> WorkerDocumentListener::Create( + WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + auto listener = MakeRefPtr<WorkerDocumentListener>(); + + RefPtr<StrongWorkerRef> strongWorkerRef = + StrongWorkerRef::Create(aWorkerPrivate, "WorkerDocumentListener", + [listener]() { listener->Destroy(); }); + if (NS_WARN_IF(!strongWorkerRef)) { + return nullptr; + } + + listener->mWorkerRef = new ThreadSafeWorkerRef(strongWorkerRef); + uint64_t windowID = aWorkerPrivate->WindowID(); + + aWorkerPrivate->DispatchToMainThread(NS_NewRunnableFunction( + "WorkerDocumentListener::Create", + [listener, windowID] { listener->SetListening(windowID, true); })); + + return listener; +} + +void WorkerDocumentListener::OnVisible(bool aVisible) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + if (!mWorkerRef) { + // We haven't handled the runnable to release this yet. + return; + } + + class VisibleRunnable final : public WorkerRunnable { + public: + VisibleRunnable(WorkerPrivate* aWorkerPrivate, bool aVisible) + : WorkerRunnable(aWorkerPrivate, "VisibleRunnable"), + mVisible(aVisible) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + WorkerGlobalScope* scope = aWorkerPrivate->GlobalScope(); + MOZ_ASSERT(scope); + scope->OnDocumentVisible(mVisible); + return true; + } + + private: + const bool mVisible; + }; + + auto runnable = MakeRefPtr<VisibleRunnable>(mWorkerRef->Private(), aVisible); + runnable->Dispatch(); +} + +void WorkerDocumentListener::SetListening(uint64_t aWindowID, bool aListen) { + MOZ_ASSERT(NS_IsMainThread()); + + auto* window = nsGlobalWindowInner::GetInnerWindowWithId(aWindowID); + Document* doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + // This would typically happen during shutdown if there is an active worker + // listening for document events. The Document may already be freed when we + // try to deregister for notifications. + return; + } + + if (aListen) { + doc->AddWorkerDocumentListener(this); + } else { + doc->RemoveWorkerDocumentListener(this); + } +} + +void WorkerDocumentListener::Destroy() { + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(mWorkerRef); + WorkerPrivate* workerPrivate = mWorkerRef->Private(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + uint64_t windowID = workerPrivate->WindowID(); + workerPrivate->DispatchToMainThread(NS_NewRunnableFunction( + "WorkerDocumentListener::Destroy", [self = RefPtr{this}, windowID] { + self->SetListening(windowID, false); + })); + mWorkerRef = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerDocumentListener.h b/dom/workers/WorkerDocumentListener.h new file mode 100644 index 0000000000..4bb46cadf2 --- /dev/null +++ b/dom/workers/WorkerDocumentListener.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WorkerDocumentListener_h__ +#define mozilla_dom_WorkerDocumentListener_h__ + +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { +class ThreadSafeWorkerRef; +class WorkerPrivate; +class WeakWorkerRef; + +class WorkerDocumentListener final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WorkerDocumentListener) + + public: + WorkerDocumentListener(); + + void OnVisible(bool aVisible); + void SetListening(uint64_t aWindowID, bool aListen); + void Destroy(); + + static RefPtr<WorkerDocumentListener> Create(WorkerPrivate* aWorkerPrivate); + + private: + ~WorkerDocumentListener(); + + Mutex mMutex MOZ_UNANNOTATED; // protects mWorkerRef + RefPtr<ThreadSafeWorkerRef> mWorkerRef; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_WorkerDocumentListener_h__ */ diff --git a/dom/workers/WorkerError.cpp b/dom/workers/WorkerError.cpp new file mode 100644 index 0000000000..1b75599211 --- /dev/null +++ b/dom/workers/WorkerError.cpp @@ -0,0 +1,452 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerError.h" + +#include <stdio.h> +#include <algorithm> +#include <utility> +#include "MainThreadUtils.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "js/ComparisonOperators.h" +#include "js/UniquePtr.h" +#include "js/friend/ErrorMessages.h" +#include "jsapi.h" +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/ArrayIterator.h" +#include "mozilla/Assertions.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Span.h" +#include "mozilla/ThreadSafeWeakPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventBinding.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/SimpleGlobalObject.h" +#include "mozilla/dom/Worker.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerGlobalScopeBinding.h" +#include "mozilla/fallible.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsGlobalWindowInner.h" +#include "nsIConsoleService.h" +#include "nsIScriptError.h" +#include "nsScriptError.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsWrapperCacheInlines.h" +#include "nscore.h" +#include "xpcpublic.h" + +namespace mozilla::dom { + +namespace { + +class ReportErrorRunnable final : public WorkerDebuggeeRunnable { + UniquePtr<WorkerErrorReport> mReport; + + public: + ReportErrorRunnable(WorkerPrivate* aWorkerPrivate, + UniquePtr<WorkerErrorReport> aReport) + : WorkerDebuggeeRunnable(aWorkerPrivate, "ReportErrorRunnable"), + mReport(std::move(aReport)) {} + + private: + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // Dispatch may fail if the worker was canceled, no need to report that as + // an error, so don't call base class PostDispatch. + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + uint64_t innerWindowId; + bool fireAtScope = true; + + bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents(); + + WorkerPrivate* parent = aWorkerPrivate->GetParent(); + if (parent) { + innerWindowId = 0; + } else { + AssertIsOnMainThread(); + + if (aWorkerPrivate->IsSharedWorker()) { + aWorkerPrivate->GetRemoteWorkerController() + ->ErrorPropagationOnMainThread(mReport.get(), + /* isErrorEvent */ true); + return true; + } + + // Service workers do not have a main thread parent global, so normal + // worker error reporting will crash. Instead, pass the error to + // the ServiceWorkerManager to report on any controlled documents. + if (aWorkerPrivate->IsServiceWorker()) { + RefPtr<RemoteWorkerChild> actor( + aWorkerPrivate->GetRemoteWorkerController()); + + Unused << NS_WARN_IF(!actor); + + if (actor) { + actor->ErrorPropagationOnMainThread(nullptr, false); + } + + return true; + } + + // The innerWindowId is only required if we are going to ReportError + // below, which is gated on this condition. The inner window correctness + // check is only going to succeed when the worker is accepting events. + if (workerIsAcceptingEvents) { + aWorkerPrivate->AssertInnerWindowIsCorrect(); + innerWindowId = aWorkerPrivate->WindowID(); + } + } + + // Don't fire this event if the JS object has been disconnected from the + // private object. + if (!workerIsAcceptingEvents) { + return true; + } + + WorkerErrorReport::ReportError(aCx, parent, fireAtScope, + aWorkerPrivate->ParentEventTargetRef(), + std::move(mReport), innerWindowId); + return true; + } +}; + +class ReportGenericErrorRunnable final : public WorkerDebuggeeRunnable { + public: + static void CreateAndDispatch(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<ReportGenericErrorRunnable> runnable = + new ReportGenericErrorRunnable(aWorkerPrivate); + runnable->Dispatch(); + } + + private: + explicit ReportGenericErrorRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerDebuggeeRunnable(aWorkerPrivate, "ReportGenericErrorRunnable") { + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // Dispatch may fail if the worker was canceled, no need to report that as + // an error, so don't call base class PostDispatch. + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + if (!aWorkerPrivate->IsAcceptingEvents()) { + return true; + } + + if (aWorkerPrivate->IsSharedWorker()) { + aWorkerPrivate->GetRemoteWorkerController()->ErrorPropagationOnMainThread( + nullptr, false); + return true; + } + + if (aWorkerPrivate->IsServiceWorker()) { + RefPtr<RemoteWorkerChild> actor( + aWorkerPrivate->GetRemoteWorkerController()); + + Unused << NS_WARN_IF(!actor); + + if (actor) { + actor->ErrorPropagationOnMainThread(nullptr, false); + } + + return true; + } + + RefPtr<mozilla::dom::EventTarget> parentEventTarget = + aWorkerPrivate->ParentEventTargetRef(); + RefPtr<Event> event = + Event::Constructor(parentEventTarget, u"error"_ns, EventInit()); + event->SetTrusted(true); + + parentEventTarget->DispatchEvent(*event); + return true; + } +}; + +} // namespace + +void WorkerErrorBase::AssignErrorBase(JSErrorBase* aReport) { + CopyUTF8toUTF16(MakeStringSpan(aReport->filename.c_str()), mFilename); + mLineNumber = aReport->lineno; + mColumnNumber = aReport->column.oneOriginValue(); + mErrorNumber = aReport->errorNumber; +} + +void WorkerErrorNote::AssignErrorNote(JSErrorNotes::Note* aNote) { + WorkerErrorBase::AssignErrorBase(aNote); + xpc::ErrorNote::ErrorNoteToMessageString(aNote, mMessage); +} + +WorkerErrorReport::WorkerErrorReport() + : mIsWarning(false), mExnType(JSEXN_ERR), mMutedError(false) {} + +void WorkerErrorReport::AssignErrorReport(JSErrorReport* aReport) { + WorkerErrorBase::AssignErrorBase(aReport); + xpc::ErrorReport::ErrorReportToMessageString(aReport, mMessage); + + mLine.Assign(aReport->linebuf(), aReport->linebufLength()); + mIsWarning = aReport->isWarning(); + MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT); + mExnType = JSExnType(aReport->exnType); + mMutedError = aReport->isMuted; + + if (aReport->notes) { + if (!mNotes.SetLength(aReport->notes->length(), fallible)) { + return; + } + + size_t i = 0; + for (auto&& note : *aReport->notes) { + mNotes.ElementAt(i).AssignErrorNote(note.get()); + i++; + } + } +} + +// aWorkerPrivate is the worker thread we're on (or the main thread, if null) +// aTarget is the worker object that we are going to fire an error at +// (if any). +/* static */ +void WorkerErrorReport::ReportError( + JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope, + DOMEventTargetHelper* aTarget, UniquePtr<WorkerErrorReport> aReport, + uint64_t aInnerWindowId, JS::Handle<JS::Value> aException) { + if (aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + } else { + AssertIsOnMainThread(); + } + + // We should not fire error events for warnings but instead make sure that + // they show up in the error console. + if (!aReport->mIsWarning) { + // First fire an ErrorEvent at the worker. + RootedDictionary<ErrorEventInit> init(aCx); + + if (aReport->mMutedError) { + init.mMessage.AssignLiteral("Script error."); + } else { + init.mMessage = aReport->mMessage; + init.mFilename = aReport->mFilename; + init.mLineno = aReport->mLineNumber; + init.mColno = aReport->mColumnNumber; + init.mError = aException; + } + + init.mCancelable = true; + init.mBubbles = false; + + if (aTarget) { + RefPtr<ErrorEvent> event = + ErrorEvent::Constructor(aTarget, u"error"_ns, init); + event->SetTrusted(true); + + bool defaultActionEnabled = + aTarget->DispatchEvent(*event, CallerType::System, IgnoreErrors()); + if (!defaultActionEnabled) { + return; + } + } + + // Now fire an event at the global object, but don't do that if the error + // code is too much recursion and this is the same script threw the error. + // XXXbz the interaction of this with worker errors seems kinda broken. + // An overrecursion in the debugger or debugger sandbox will get turned + // into an error event on our parent worker! + // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this + // better. + if (aFireAtScope && + (aTarget || aReport->mErrorNumber != JSMSG_OVER_RECURSED)) { + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + NS_ASSERTION(global, "This should never be null!"); + + nsEventStatus status = nsEventStatus_eIgnore; + + if (aWorkerPrivate) { + RefPtr<WorkerGlobalScope> globalScope; + UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope); + + if (!globalScope) { + WorkerDebuggerGlobalScope* globalScope = nullptr; + UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope); + + MOZ_ASSERT_IF(globalScope, + globalScope->GetWrapperPreserveColor() == global); + if (globalScope || IsWorkerDebuggerSandbox(global)) { + aWorkerPrivate->ReportErrorToDebugger( + aReport->mFilename, aReport->mLineNumber, aReport->mMessage); + return; + } + + MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) == + SimpleGlobalObject::GlobalType::BindingDetail); + // XXXbz We should really log this to console, but unwinding out of + // this stuff without ending up firing any events is ... hard. Just + // return for now. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks + // making this better. + return; + } + + MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global); + + RefPtr<ErrorEvent> event = + ErrorEvent::Constructor(aTarget, u"error"_ns, init); + event->SetTrusted(true); + + if (NS_FAILED(EventDispatcher::DispatchDOMEvent( + globalScope, nullptr, event, nullptr, &status))) { + NS_WARNING("Failed to dispatch worker thread error event!"); + status = nsEventStatus_eIgnore; + } + } else if (nsGlobalWindowInner* win = xpc::WindowOrNull(global)) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!win->HandleScriptError(init, &status)) { + NS_WARNING("Failed to dispatch main thread error event!"); + status = nsEventStatus_eIgnore; + } + } + + // Was preventDefault() called? + if (status == nsEventStatus_eConsumeNoDefault) { + return; + } + } + } + + // Now fire a runnable to do the same on the parent's thread if we can. + if (aWorkerPrivate) { + RefPtr<ReportErrorRunnable> runnable = + new ReportErrorRunnable(aWorkerPrivate, std::move(aReport)); + runnable->Dispatch(); + return; + } + + // Otherwise log an error to the error console. + WorkerErrorReport::LogErrorToConsole(aCx, *aReport, aInnerWindowId); +} + +/* static */ +void WorkerErrorReport::LogErrorToConsole(JSContext* aCx, + WorkerErrorReport& aReport, + uint64_t aInnerWindowId) { + JS::Rooted<JSObject*> stack(aCx, aReport.ReadStack(aCx)); + JS::Rooted<JSObject*> stackGlobal(aCx, JS::CurrentGlobalOrNull(aCx)); + + ErrorData errorData( + aReport.mIsWarning, aReport.mLineNumber, aReport.mColumnNumber, + aReport.mMessage, aReport.mFilename, aReport.mLine, + TransformIntoNewArray(aReport.mNotes, [](const WorkerErrorNote& note) { + return ErrorDataNote(note.mLineNumber, note.mColumnNumber, + note.mMessage, note.mFilename); + })); + LogErrorToConsole(errorData, aInnerWindowId, stack, stackGlobal); +} + +/* static */ +void WorkerErrorReport::LogErrorToConsole(const ErrorData& aReport, + uint64_t aInnerWindowId, + JS::Handle<JSObject*> aStack, + JS::Handle<JSObject*> aStackGlobal) { + AssertIsOnMainThread(); + + RefPtr<nsScriptErrorBase> scriptError = + CreateScriptError(nullptr, JS::NothingHandleValue, aStack, aStackGlobal); + + NS_WARNING_ASSERTION(scriptError, "Failed to create script error!"); + + if (scriptError) { + nsAutoCString category("Web Worker"); + uint32_t flags = aReport.isWarning() ? nsIScriptError::warningFlag + : nsIScriptError::errorFlag; + if (NS_FAILED(scriptError->nsIScriptError::InitWithWindowID( + aReport.message(), aReport.filename(), aReport.line(), + aReport.lineNumber(), aReport.columnNumber(), flags, category, + aInnerWindowId))) { + NS_WARNING("Failed to init script error!"); + scriptError = nullptr; + } + + for (const ErrorDataNote& note : aReport.notes()) { + nsScriptErrorNote* noteObject = new nsScriptErrorNote(); + noteObject->Init(note.message(), note.filename(), 0, note.lineNumber(), + note.columnNumber()); + scriptError->AddNote(noteObject); + } + } + + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + NS_WARNING_ASSERTION(consoleService, "Failed to get console service!"); + + if (consoleService) { + if (scriptError) { + if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) { + return; + } + NS_WARNING("LogMessage failed!"); + } else if (NS_SUCCEEDED(consoleService->LogStringMessage( + aReport.message().BeginReading()))) { + return; + } + NS_WARNING("LogStringMessage failed!"); + } + + NS_ConvertUTF16toUTF8 msg(aReport.message()); + NS_ConvertUTF16toUTF8 filename(aReport.filename()); + + static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]"; + +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(), + filename.get(), aReport.lineNumber()); +#endif + + fprintf(stderr, kErrorString, msg.get(), filename.get(), + aReport.lineNumber()); + fflush(stderr); +} + +/* static */ +void WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent( + WorkerPrivate* aWorkerPrivate) { + ReportGenericErrorRunnable::CreateAndDispatch(aWorkerPrivate); +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerError.h b/dom/workers/WorkerError.h new file mode 100644 index 0000000000..43600fbf1a --- /dev/null +++ b/dom/workers/WorkerError.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerError_h +#define mozilla_dom_workers_WorkerError_h + +#include "mozilla/dom/SerializedStackHolder.h" +#include "mozilla/dom/WorkerCommon.h" +#include "jsapi.h" + +namespace mozilla { + +class DOMEventTargetHelper; + +namespace dom { + +class ErrorData; +class WorkerErrorBase { + public: + nsString mMessage; + nsString mFilename; + // Line number (1-origin). + uint32_t mLineNumber; + // Column number in UTF-16 code units (1-origin). + uint32_t mColumnNumber; + uint32_t mErrorNumber; + + WorkerErrorBase() : mLineNumber(0), mColumnNumber(0), mErrorNumber(0) {} + + void AssignErrorBase(JSErrorBase* aReport); +}; + +class WorkerErrorNote : public WorkerErrorBase { + public: + void AssignErrorNote(JSErrorNotes::Note* aNote); +}; + +class WorkerPrivate; + +class WorkerErrorReport : public WorkerErrorBase, public SerializedStackHolder { + public: + nsString mLine; + bool mIsWarning; + JSExnType mExnType; + bool mMutedError; + nsTArray<WorkerErrorNote> mNotes; + + WorkerErrorReport(); + + void AssignErrorReport(JSErrorReport* aReport); + + // aWorkerPrivate is the worker thread we're on (or the main thread, if null) + // aTarget is the worker object that we are going to fire an error at + // (if any). + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1743443) + MOZ_CAN_RUN_SCRIPT_BOUNDARY static void ReportError( + JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope, + DOMEventTargetHelper* aTarget, UniquePtr<WorkerErrorReport> aReport, + uint64_t aInnerWindowId, + JS::Handle<JS::Value> aException = JS::NullHandleValue); + + static void LogErrorToConsole(JSContext* aCx, WorkerErrorReport& aReport, + uint64_t aInnerWindowId); + + static void LogErrorToConsole(const mozilla::dom::ErrorData& aReport, + uint64_t aInnerWindowId, + JS::Handle<JSObject*> aStack = nullptr, + JS::Handle<JSObject*> aStackGlobal = nullptr); + + static void CreateAndDispatchGenericErrorRunnableToParent( + WorkerPrivate* aWorkerPrivate); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_WorkerError_h diff --git a/dom/workers/WorkerEventTarget.cpp b/dom/workers/WorkerEventTarget.cpp new file mode 100644 index 0000000000..cb58b4f8ed --- /dev/null +++ b/dom/workers/WorkerEventTarget.cpp @@ -0,0 +1,199 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerEventTarget.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" + +#include "mozilla/Logging.h" +#include "mozilla/dom/ReferrerInfo.h" + +namespace mozilla::dom { + +static mozilla::LazyLogModule sWorkerEventTargetLog("WorkerEventTarget"); + +#ifdef LOG +# undef LOG +#endif +#ifdef LOGV +# undef LOGV +#endif +#define LOG(args) MOZ_LOG(sWorkerEventTargetLog, LogLevel::Debug, args); +#define LOGV(args) MOZ_LOG(sWorkerEventTargetLog, LogLevel::Verbose, args); + +namespace { + +class WrappedControlRunnable final : public WorkerControlRunnable { + nsCOMPtr<nsIRunnable> mInner; + + ~WrappedControlRunnable() = default; + + public: + WrappedControlRunnable(WorkerPrivate* aWorkerPrivate, + nsCOMPtr<nsIRunnable>&& aInner) + : WorkerControlRunnable(aWorkerPrivate, "WrappedControlRunnable", + WorkerThread), + mInner(std::move(aInner)) {} + + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + // Silence bad assertions, this can be dispatched from any thread. + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // Silence bad assertions, this can be dispatched from any thread. + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + mInner->Run(); + return true; + } + + nsresult Cancel() override { + nsCOMPtr<nsICancelableRunnable> cr = do_QueryInterface(mInner); + + // If the inner runnable is not cancellable, then just do the normal + // WorkerControlRunnable thing. This will end up calling Run(). + if (!cr) { + return Run(); + } + + // Otherwise call the inner runnable's Cancel() and treat this like + // a WorkerRunnable cancel. We can't call WorkerControlRunnable::Cancel() + // in this case since that would result in both Run() and the inner + // Cancel() being called. + return cr->Cancel(); + } +}; + +} // anonymous namespace + +NS_IMPL_ISUPPORTS(WorkerEventTarget, nsIEventTarget, nsISerialEventTarget) + +WorkerEventTarget::WorkerEventTarget(WorkerPrivate* aWorkerPrivate, + Behavior aBehavior) + : mMutex("WorkerEventTarget"), + mWorkerPrivate(aWorkerPrivate), + mBehavior(aBehavior) { + LOG(("WorkerEventTarget::WorkerEventTarget [%p] aBehavior: %u", this, + (uint8_t)aBehavior)); + MOZ_DIAGNOSTIC_ASSERT(mWorkerPrivate); +} + +void WorkerEventTarget::ForgetWorkerPrivate(WorkerPrivate* aWorkerPrivate) { + LOG(("WorkerEventTarget::ForgetWorkerPrivate [%p] aWorkerPrivate: %p", this, + aWorkerPrivate)); + MutexAutoLock lock(mMutex); + MOZ_DIAGNOSTIC_ASSERT(!mWorkerPrivate || mWorkerPrivate == aWorkerPrivate); + mWorkerPrivate = nullptr; +} + +NS_IMETHODIMP +WorkerEventTarget::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) { + LOGV(("WorkerEventTarget::DispatchFromScript [%p] aRunnable: %p", this, + aRunnable)); + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +WorkerEventTarget::Dispatch(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + LOGV( + ("WorkerEventTarget::Dispatch [%p] aRunnable: %p", this, runnable.get())); + + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + return NS_ERROR_FAILURE; + } + + if (mBehavior == Behavior::Hybrid) { + LOGV(("WorkerEventTarget::Dispatch [%p] Dispatch as normal runnable(%p)", + this, runnable.get())); + + RefPtr<WorkerRunnable> r = + mWorkerPrivate->MaybeWrapAsWorkerRunnable(runnable.forget()); + if (r->Dispatch()) { + return NS_OK; + } + runnable = std::move(r); + LOGV(( + "WorkerEventTarget::Dispatch [%p] Dispatch as normal runnable(%p) fail", + this, runnable.get())); + } + + RefPtr<WorkerControlRunnable> r = + new WrappedControlRunnable(mWorkerPrivate, std::move(runnable)); + LOGV( + ("WorkerEventTarget::Dispatch [%p] Wrapped runnable as control " + "runnable(%p)", + this, r.get())); + if (!r->Dispatch()) { + LOGV( + ("WorkerEventTarget::Dispatch [%p] Dispatch as control runnable(%p) " + "fail", + this, r.get())); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerEventTarget::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WorkerEventTarget::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + + MutexAutoLock lock(mMutex); + + // If mWorkerPrivate is gone, the event target is already late during + // shutdown, return NS_ERROR_UNEXPECTED as documented in `nsIEventTarget.idl`. + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + return mWorkerPrivate->RegisterShutdownTask(aTask); +} + +NS_IMETHODIMP +WorkerEventTarget::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + return mWorkerPrivate->UnregisterShutdownTask(aTask); +} + +NS_IMETHODIMP_(bool) +WorkerEventTarget::IsOnCurrentThreadInfallible() { + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + return false; + } + + return mWorkerPrivate->IsOnCurrentThread(); +} + +NS_IMETHODIMP +WorkerEventTarget::IsOnCurrentThread(bool* aIsOnCurrentThread) { + MOZ_ASSERT(aIsOnCurrentThread); + *aIsOnCurrentThread = IsOnCurrentThreadInfallible(); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerEventTarget.h b/dom/workers/WorkerEventTarget.h new file mode 100644 index 0000000000..dd9cb61748 --- /dev/null +++ b/dom/workers/WorkerEventTarget.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WorkerEventTarget_h +#define mozilla_dom_WorkerEventTarget_h + +#include "nsISerialEventTarget.h" +#include "mozilla/Mutex.h" +#include "mozilla/dom/WorkerPrivate.h" + +namespace mozilla::dom { + +class WorkerEventTarget final : public nsISerialEventTarget { + public: + // The WorkerEventTarget supports different dispatch behaviors: + // + // * Hybrid targets will attempt to dispatch as a normal runnable, + // but fallback to a control runnable if that fails. This is + // often necessary for code that wants normal dispatch order, but + // also needs to execute while the worker is shutting down (possibly + // with a holder in place.) + // + // * ControlOnly targets will simply dispatch a control runnable. + enum class Behavior : uint8_t { Hybrid, ControlOnly }; + + private: + mozilla::Mutex mMutex; + CheckedUnsafePtr<WorkerPrivate> mWorkerPrivate MOZ_GUARDED_BY(mMutex); + const Behavior mBehavior MOZ_GUARDED_BY(mMutex); + + ~WorkerEventTarget() = default; + + public: + WorkerEventTarget(WorkerPrivate* aWorkerPrivate, Behavior aBehavior); + + void ForgetWorkerPrivate(WorkerPrivate* aWorkerPrivate); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET + NS_DECL_NSISERIALEVENTTARGET +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_WorkerEventTarget_h diff --git a/dom/workers/WorkerIPCUtils.h b/dom/workers/WorkerIPCUtils.h new file mode 100644 index 0000000000..0be45b307f --- /dev/null +++ b/dom/workers/WorkerIPCUtils.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef _mozilla_dom_WorkerIPCUtils_h +#define _mozilla_dom_WorkerIPCUtils_h + +#include "ipc/EnumSerializer.h" + +// Undo X11/X.h's definition of None +#undef None + +#include "mozilla/dom/WorkerBinding.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::WorkerType> + : public ContiguousEnumSerializer<mozilla::dom::WorkerType, + mozilla::dom::WorkerType::Classic, + mozilla::dom::WorkerType::EndGuard_> {}; + +} // namespace IPC + +#endif // _mozilla_dom_WorkerIPCUtils_h diff --git a/dom/workers/WorkerLoadInfo.cpp b/dom/workers/WorkerLoadInfo.cpp new file mode 100644 index 0000000000..0dec07a675 --- /dev/null +++ b/dom/workers/WorkerLoadInfo.cpp @@ -0,0 +1,508 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerLoadInfo.h" +#include "WorkerPrivate.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/LoadContext.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsContentUtils.h" +#include "nsIContentSecurityPolicy.h" +#include "nsICookieJarSettings.h" +#include "nsINetworkInterceptController.h" +#include "nsIProtocolHandler.h" +#include "nsIReferrerInfo.h" +#include "nsIBrowserChild.h" +#include "nsScriptSecurityManager.h" +#include "nsNetUtil.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +class MainThreadReleaseRunnable final : public Runnable { + nsTArray<nsCOMPtr<nsISupports>> mDoomed; + nsCOMPtr<nsILoadGroup> mLoadGroupToCancel; + + public: + MainThreadReleaseRunnable(nsTArray<nsCOMPtr<nsISupports>>&& aDoomed, + nsCOMPtr<nsILoadGroup>&& aLoadGroupToCancel) + : mozilla::Runnable("MainThreadReleaseRunnable"), + mDoomed(std::move(aDoomed)), + mLoadGroupToCancel(std::move(aLoadGroupToCancel)) {} + + NS_INLINE_DECL_REFCOUNTING_INHERITED(MainThreadReleaseRunnable, Runnable) + + NS_IMETHOD + Run() override { + if (mLoadGroupToCancel) { + mLoadGroupToCancel->CancelWithReason( + NS_BINDING_ABORTED, "WorkerLoadInfo::MainThreadReleaseRunnable"_ns); + mLoadGroupToCancel = nullptr; + } + + mDoomed.Clear(); + return NS_OK; + } + + private: + ~MainThreadReleaseRunnable() = default; +}; + +// Specialize this if there's some class that has multiple nsISupports bases. +template <class T> +struct ISupportsBaseInfo { + using ISupportsBase = T; +}; + +template <template <class> class SmartPtr, class T> +inline void SwapToISupportsArray(SmartPtr<T>& aSrc, + nsTArray<nsCOMPtr<nsISupports>>& aDest) { + nsCOMPtr<nsISupports>* dest = aDest.AppendElement(); + + T* raw = nullptr; + aSrc.swap(raw); + + nsISupports* rawSupports = + static_cast<typename ISupportsBaseInfo<T>::ISupportsBase*>(raw); + dest->swap(rawSupports); +} + +} // namespace + +WorkerLoadInfoData::WorkerLoadInfoData() + : mLoadFlags(nsIRequest::LOAD_NORMAL), + mWindowID(UINT64_MAX), + mAssociatedBrowsingContextID(0), + mReferrerInfo(new ReferrerInfo(nullptr)), + mFromWindow(false), + mEvalAllowed(false), + mReportEvalCSPViolations(false), + mWasmEvalAllowed(false), + mReportWasmEvalCSPViolations(false), + mXHRParamsAllowed(false), + mWatchedByDevTools(false), + mStorageAccess(StorageAccess::eDeny), + mUseRegularPrincipal(false), + mUsingStorageAccess(false), + mServiceWorkersTestingInWindow(false), + mShouldResistFingerprinting(false), + mIsThirdPartyContextToTopWindow(true), + mSecureContext(eNotSet) {} + +nsresult WorkerLoadInfo::SetPrincipalsAndCSPOnMainThread( + nsIPrincipal* aPrincipal, nsIPrincipal* aPartitionedPrincipal, + nsILoadGroup* aLoadGroup, nsIContentSecurityPolicy* aCsp) { + AssertIsOnMainThread(); + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(aLoadGroup, aPrincipal)); + + mPrincipal = aPrincipal; + mPartitionedPrincipal = aPartitionedPrincipal; + + mCSP = aCsp; + + if (mCSP) { + mCSP->GetAllowsEval(&mReportEvalCSPViolations, &mEvalAllowed); + mCSP->GetAllowsWasmEval(&mReportWasmEvalCSPViolations, &mWasmEvalAllowed); + mCSPInfo = MakeUnique<CSPInfo>(); + nsresult rv = CSPToCSPInfo(aCsp, mCSPInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + mEvalAllowed = true; + mReportEvalCSPViolations = false; + mWasmEvalAllowed = true; + mReportWasmEvalCSPViolations = false; + } + + mLoadGroup = aLoadGroup; + + mPrincipalInfo = MakeUnique<PrincipalInfo>(); + mPartitionedPrincipalInfo = MakeUnique<PrincipalInfo>(); + StoragePrincipalHelper::GetRegularPrincipalOriginAttributes( + aLoadGroup, mOriginAttributes); + + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, mPrincipalInfo.get()); + NS_ENSURE_SUCCESS(rv, rv); + + if (aPrincipal->Equals(aPartitionedPrincipal)) { + *mPartitionedPrincipalInfo = *mPrincipalInfo; + } else { + mPartitionedPrincipalInfo = MakeUnique<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(aPartitionedPrincipal, + mPartitionedPrincipalInfo.get()); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult WorkerLoadInfo::GetPrincipalsAndLoadGroupFromChannel( + nsIChannel* aChannel, nsIPrincipal** aPrincipalOut, + nsIPrincipal** aPartitionedPrincipalOut, nsILoadGroup** aLoadGroupOut) { + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(aChannel); + MOZ_DIAGNOSTIC_ASSERT(aPrincipalOut); + MOZ_DIAGNOSTIC_ASSERT(aPartitionedPrincipalOut); + MOZ_DIAGNOSTIC_ASSERT(aLoadGroupOut); + + // Initial triggering principal should be set + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_DOM_INVALID_STATE_ERR); + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + MOZ_DIAGNOSTIC_ASSERT(ssm); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + nsCOMPtr<nsIPrincipal> channelPartitionedPrincipal; + nsresult rv = ssm->GetChannelResultPrincipals( + aChannel, getter_AddRefs(channelPrincipal), + getter_AddRefs(channelPartitionedPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + // Every time we call GetChannelResultPrincipal() it will return a different + // null principal for a data URL. We don't want to change the worker's + // principal again, though. Instead just keep the original null principal we + // first got from the channel. + // + // Note, we don't do this by setting principalToInherit on the channel's + // load info because we don't yet have the first null principal when we + // create the channel. + if (mPrincipal && mPrincipal->GetIsNullPrincipal() && + channelPrincipal->GetIsNullPrincipal()) { + channelPrincipal = mPrincipal; + channelPartitionedPrincipal = mPrincipal; + } + + nsCOMPtr<nsILoadGroup> channelLoadGroup; + rv = aChannel->GetLoadGroup(getter_AddRefs(channelLoadGroup)); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(channelLoadGroup); + + // If the loading principal is the system principal then the channel + // principal must also be the system principal (we do not allow chrome + // code to create workers with non-chrome scripts, and if we ever decide + // to change this we need to make sure we don't always set + // mPrincipalIsSystem to true in WorkerPrivate::GetLoadInfo()). Otherwise + // this channel principal must be same origin with the load principal (we + // check again here in case redirects changed the location of the script). + if (mLoadingPrincipal->IsSystemPrincipal()) { + if (!channelPrincipal->IsSystemPrincipal()) { + nsCOMPtr<nsIURI> finalURI; + rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + // See if this is a resource URI. Since JSMs usually come from + // resource:// URIs we're currently considering all URIs with the + // URI_IS_UI_RESOURCE flag as valid for creating privileged workers. + bool isResource; + rv = NS_URIChainHasFlags(finalURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, + &isResource); + NS_ENSURE_SUCCESS(rv, rv); + + if (isResource) { + // Assign the system principal to the resource:// worker only if it + // was loaded from code using the system principal. + channelPrincipal = mLoadingPrincipal; + channelPartitionedPrincipal = mLoadingPrincipal; + } else { + return NS_ERROR_DOM_BAD_URI; + } + } + } + + // The principal can change, but it should still match the original + // load group's browser element flag. + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(channelLoadGroup, channelPrincipal)); + + channelPrincipal.forget(aPrincipalOut); + channelPartitionedPrincipal.forget(aPartitionedPrincipalOut); + channelLoadGroup.forget(aLoadGroupOut); + + return NS_OK; +} + +nsresult WorkerLoadInfo::SetPrincipalsAndCSPFromChannel(nsIChannel* aChannel) { + AssertIsOnMainThread(); + + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIPrincipal> partitionedPrincipal; + nsCOMPtr<nsILoadGroup> loadGroup; + nsresult rv = GetPrincipalsAndLoadGroupFromChannel( + aChannel, getter_AddRefs(principal), getter_AddRefs(partitionedPrincipal), + getter_AddRefs(loadGroup)); + NS_ENSURE_SUCCESS(rv, rv); + + // Workers themselves can have their own CSP - Workers of an opaque origin + // however inherit the CSP of the document that spawned the worker. + nsCOMPtr<nsIContentSecurityPolicy> csp; + if (CSP_ShouldResponseInheritCSP(aChannel)) { + nsCOMPtr<nsILoadInfo> loadinfo = aChannel->LoadInfo(); + csp = loadinfo->GetCsp(); + } + return SetPrincipalsAndCSPOnMainThread(principal, partitionedPrincipal, + loadGroup, csp); +} + +bool WorkerLoadInfo::FinalChannelPrincipalIsValid(nsIChannel* aChannel) { + AssertIsOnMainThread(); + + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIPrincipal> partitionedPrincipal; + nsCOMPtr<nsILoadGroup> loadGroup; + nsresult rv = GetPrincipalsAndLoadGroupFromChannel( + aChannel, getter_AddRefs(principal), getter_AddRefs(partitionedPrincipal), + getter_AddRefs(loadGroup)); + NS_ENSURE_SUCCESS(rv, false); + + // Verify that the channel is still a null principal. We don't care + // if these are the exact same null principal object, though. From + // the worker's perspective its the same effect. + if (principal->GetIsNullPrincipal() && mPrincipal->GetIsNullPrincipal()) { + return true; + } + + // Otherwise we require exact equality. Redirects can happen, but they + // are not allowed to change our principal. + if (principal->Equals(mPrincipal)) { + return true; + } + + return false; +} + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED +bool WorkerLoadInfo::PrincipalIsValid() const { + return mPrincipal && mPrincipalInfo && + mPrincipalInfo->type() != PrincipalInfo::T__None && + mPrincipalInfo->type() <= PrincipalInfo::T__Last && + mPartitionedPrincipal && mPartitionedPrincipalInfo && + mPartitionedPrincipalInfo->type() != PrincipalInfo::T__None && + mPartitionedPrincipalInfo->type() <= PrincipalInfo::T__Last; +} + +bool WorkerLoadInfo::PrincipalURIMatchesScriptURL() { + AssertIsOnMainThread(); + + nsAutoCString scheme; + nsresult rv = mBaseURI->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, false); + + // A system principal must either be a blob URL or a resource JSM. + if (mPrincipal->IsSystemPrincipal()) { + if (scheme == "blob"_ns) { + return true; + } + + bool isResource = false; + nsresult rv = NS_URIChainHasFlags( + mBaseURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isResource); + NS_ENSURE_SUCCESS(rv, false); + + return isResource; + } + + // A null principal can occur for a data URL worker script or a blob URL + // worker script from a sandboxed iframe. + if (mPrincipal->GetIsNullPrincipal()) { + return scheme == "data"_ns || scheme == "blob"_ns; + } + + // The principal for a blob: URL worker script does not have a matching URL. + // This is likely a bug in our referer setting logic, but exempt it for now. + // This is another reason we should fix bug 1340694 so that referer does not + // depend on the principal URI. + if (scheme == "blob"_ns) { + return true; + } + + if (mPrincipal->IsSameOrigin(mBaseURI)) { + return true; + } + + // If strict file origin policy is in effect, local files will always fail + // IsSameOrigin unless they are identical. Explicitly check file origin + // policy, in that case. + + bool allowsRelaxedOriginPolicy = false; + rv = mPrincipal->AllowsRelaxStrictFileOriginPolicy( + mBaseURI, &allowsRelaxedOriginPolicy); + + if (nsScriptSecurityManager::GetStrictFileOriginPolicy() && + NS_URIIsLocalFile(mBaseURI) && + (NS_SUCCEEDED(rv) && allowsRelaxedOriginPolicy)) { + return true; + } + + return false; +} +#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED + +bool WorkerLoadInfo::ProxyReleaseMainThreadObjects( + WorkerPrivate* aWorkerPrivate) { + nsCOMPtr<nsILoadGroup> nullLoadGroup; + return ProxyReleaseMainThreadObjects(aWorkerPrivate, + std::move(nullLoadGroup)); +} + +bool WorkerLoadInfo::ProxyReleaseMainThreadObjects( + WorkerPrivate* aWorkerPrivate, + nsCOMPtr<nsILoadGroup>&& aLoadGroupToCancel) { + static const uint32_t kDoomedCount = 11; + nsTArray<nsCOMPtr<nsISupports>> doomed(kDoomedCount); + + SwapToISupportsArray(mWindow, doomed); + SwapToISupportsArray(mScriptContext, doomed); + SwapToISupportsArray(mBaseURI, doomed); + SwapToISupportsArray(mResolvedScriptURI, doomed); + SwapToISupportsArray(mPrincipal, doomed); + SwapToISupportsArray(mPartitionedPrincipal, doomed); + SwapToISupportsArray(mLoadingPrincipal, doomed); + SwapToISupportsArray(mChannel, doomed); + SwapToISupportsArray(mCSP, doomed); + SwapToISupportsArray(mLoadGroup, doomed); + SwapToISupportsArray(mInterfaceRequestor, doomed); + // Before adding anything here update kDoomedCount above! + + MOZ_ASSERT(doomed.Length() == kDoomedCount); + + RefPtr<MainThreadReleaseRunnable> runnable = new MainThreadReleaseRunnable( + std::move(doomed), std::move(aLoadGroupToCancel)); + return NS_SUCCEEDED(aWorkerPrivate->DispatchToMainThread(runnable.forget())); +} + +WorkerLoadInfo::InterfaceRequestor::InterfaceRequestor( + nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + // Look for an existing LoadContext. This is optional and it's ok if + // we don't find one. + nsCOMPtr<nsILoadContext> baseContext; + if (aLoadGroup) { + nsCOMPtr<nsIInterfaceRequestor> callbacks; + aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks)); + if (callbacks) { + callbacks->GetInterface(NS_GET_IID(nsILoadContext), + getter_AddRefs(baseContext)); + } + mOuterRequestor = callbacks; + } + + mLoadContext = new LoadContext(aPrincipal, baseContext); +} + +void WorkerLoadInfo::InterfaceRequestor::MaybeAddBrowserChild( + nsILoadGroup* aLoadGroup) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aLoadGroup) { + return; + } + + nsCOMPtr<nsIInterfaceRequestor> callbacks; + aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks)); + if (!callbacks) { + return; + } + + nsCOMPtr<nsIBrowserChild> browserChild; + callbacks->GetInterface(NS_GET_IID(nsIBrowserChild), + getter_AddRefs(browserChild)); + if (!browserChild) { + return; + } + + // Use weak references to the tab child. Holding a strong reference will + // not prevent an ActorDestroy() from being called on the BrowserChild. + // Therefore, we should let the BrowserChild destroy itself as soon as + // possible. + mBrowserChildList.AppendElement(do_GetWeakReference(browserChild)); +} + +NS_IMETHODIMP +WorkerLoadInfo::InterfaceRequestor::GetInterface(const nsIID& aIID, + void** aSink) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mLoadContext); + + if (aIID.Equals(NS_GET_IID(nsILoadContext))) { + nsCOMPtr<nsILoadContext> ref = mLoadContext; + ref.forget(aSink); + return NS_OK; + } + + // If we still have an active nsIBrowserChild, then return it. Its possible, + // though, that all of the BrowserChild objects have been destroyed. In that + // case we return NS_NOINTERFACE. + if (aIID.Equals(NS_GET_IID(nsIBrowserChild))) { + nsCOMPtr<nsIBrowserChild> browserChild = GetAnyLiveBrowserChild(); + if (!browserChild) { + return NS_NOINTERFACE; + } + browserChild.forget(aSink); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) && + mOuterRequestor) { + // If asked for the network intercept controller, ask the outer requestor, + // which could be the docshell. + return mOuterRequestor->GetInterface(aIID, aSink); + } + + return NS_NOINTERFACE; +} + +already_AddRefed<nsIBrowserChild> +WorkerLoadInfo::InterfaceRequestor::GetAnyLiveBrowserChild() { + MOZ_ASSERT(NS_IsMainThread()); + + // Search our list of known BrowserChild objects for one that still exists. + while (!mBrowserChildList.IsEmpty()) { + nsCOMPtr<nsIBrowserChild> browserChild = + do_QueryReferent(mBrowserChildList.LastElement()); + + // Does this tab child still exist? If so, return it. We are done. If the + // PBrowser actor is no longer useful, don't bother returning this tab. + if (browserChild && + !static_cast<BrowserChild*>(browserChild.get())->IsDestroyed()) { + return browserChild.forget(); + } + + // Otherwise remove the stale weak reference and check the next one + mBrowserChildList.RemoveLastElement(); + } + + return nullptr; +} + +NS_IMPL_ADDREF(WorkerLoadInfo::InterfaceRequestor) +NS_IMPL_RELEASE(WorkerLoadInfo::InterfaceRequestor) +NS_IMPL_QUERY_INTERFACE(WorkerLoadInfo::InterfaceRequestor, + nsIInterfaceRequestor) + +WorkerLoadInfo::WorkerLoadInfo() { MOZ_COUNT_CTOR(WorkerLoadInfo); } + +WorkerLoadInfo::WorkerLoadInfo(WorkerLoadInfo&& aOther) noexcept + : WorkerLoadInfoData(std::move(aOther)) { + MOZ_COUNT_CTOR(WorkerLoadInfo); +} + +WorkerLoadInfo::~WorkerLoadInfo() { MOZ_COUNT_DTOR(WorkerLoadInfo); } + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/WorkerLoadInfo.h b/dom/workers/WorkerLoadInfo.h new file mode 100644 index 0000000000..722e71d6f3 --- /dev/null +++ b/dom/workers/WorkerLoadInfo.h @@ -0,0 +1,200 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerLoadInfo_h +#define mozilla_dom_workers_WorkerLoadInfo_h + +#include "mozilla/OriginAttributes.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/OriginTrials.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h" +#include "mozilla/dom/WorkerCommon.h" + +#include "nsIInterfaceRequestor.h" +#include "nsILoadContext.h" +#include "nsIRequest.h" +#include "nsISupportsImpl.h" +#include "nsIWeakReferenceUtils.h" +#include "nsRFPService.h" +#include "nsTArray.h" + +class nsIChannel; +class nsIContentSecurityPolicy; +class nsICookieJarSettings; +class nsILoadGroup; +class nsIPrincipal; +class nsIReferrerInfo; +class nsIRunnable; +class nsIScriptContext; +class nsIBrowserChild; +class nsIURI; +class nsPIDOMWindowInner; + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +class CSPInfo; +} // namespace ipc + +namespace dom { + +class WorkerPrivate; + +struct WorkerLoadInfoData { + // All of these should be released in + // WorkerPrivateParent::ForgetMainThreadObjects. + nsCOMPtr<nsIURI> mBaseURI; + nsCOMPtr<nsIURI> mResolvedScriptURI; + + // This is the principal of the global (parent worker or a window) loading + // the worker. It can be null if we are executing a ServiceWorker, otherwise, + // except for data: URL, it must subsumes the worker principal. + // If we load a data: URL, mPrincipal will be a null principal. + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsIPrincipal> mPartitionedPrincipal; + + // Taken from the parent context. + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + + // The CookieJarSettingsArgs of mCookieJarSettings. + // This is specific for accessing on worker thread. + net::CookieJarSettingsArgs mCookieJarSettingsArgs; + + nsCOMPtr<nsIScriptContext> mScriptContext; + nsCOMPtr<nsPIDOMWindowInner> mWindow; + nsCOMPtr<nsIContentSecurityPolicy> mCSP; + // Thread boundaries require us to not only store a CSP object, but also a + // serialized version of the CSP. Reason being: Serializing a CSP to a CSPInfo + // needs to happen on the main thread, but storing the CSPInfo needs to happen + // on the worker thread. We move the CSPInfo into the Client within + // ScriptLoader::PreRun(). + UniquePtr<mozilla::ipc::CSPInfo> mCSPInfo; + + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsILoadGroup> mLoadGroup; + + class InterfaceRequestor final : public nsIInterfaceRequestor { + NS_DECL_ISUPPORTS + + public: + InterfaceRequestor(nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup); + void MaybeAddBrowserChild(nsILoadGroup* aLoadGroup); + NS_IMETHOD GetInterface(const nsIID& aIID, void** aSink) override; + + void SetOuterRequestor(nsIInterfaceRequestor* aOuterRequestor) { + MOZ_ASSERT(!mOuterRequestor); + MOZ_ASSERT(aOuterRequestor); + mOuterRequestor = aOuterRequestor; + } + + private: + ~InterfaceRequestor() = default; + + already_AddRefed<nsIBrowserChild> GetAnyLiveBrowserChild(); + + nsCOMPtr<nsILoadContext> mLoadContext; + nsCOMPtr<nsIInterfaceRequestor> mOuterRequestor; + + // Array of weak references to nsIBrowserChild. We do not want to keep + // BrowserChild actors alive for long after their ActorDestroy() methods are + // called. + nsTArray<nsWeakPtr> mBrowserChildList; + }; + + // Only set if we have a custom overriden load group + RefPtr<InterfaceRequestor> mInterfaceRequestor; + + UniquePtr<mozilla::ipc::PrincipalInfo> mPrincipalInfo; + UniquePtr<mozilla::ipc::PrincipalInfo> mPartitionedPrincipalInfo; + nsCString mDomain; + + nsString mServiceWorkerCacheName; + Maybe<ServiceWorkerDescriptor> mServiceWorkerDescriptor; + Maybe<ServiceWorkerRegistrationDescriptor> + mServiceWorkerRegistrationDescriptor; + + Maybe<ServiceWorkerDescriptor> mParentController; + + nsID mAgentClusterId; + + ChannelInfo mChannelInfo; + nsLoadFlags mLoadFlags; + + uint64_t mWindowID; + uint64_t mAssociatedBrowsingContextID; + + nsCOMPtr<nsIReferrerInfo> mReferrerInfo; + OriginTrials mTrials; + bool mFromWindow; + bool mEvalAllowed; + bool mReportEvalCSPViolations; + bool mWasmEvalAllowed; + bool mReportWasmEvalCSPViolations; + bool mXHRParamsAllowed; + bool mWatchedByDevTools; + StorageAccess mStorageAccess; + bool mUseRegularPrincipal; + bool mUsingStorageAccess; + bool mServiceWorkersTestingInWindow; + bool mShouldResistFingerprinting; + Maybe<RFPTarget> mOverriddenFingerprintingSettings; + OriginAttributes mOriginAttributes; + bool mIsThirdPartyContextToTopWindow; + + enum { + eNotSet, + eInsecureContext, + eSecureContext, + } mSecureContext; + + WorkerLoadInfoData(); + WorkerLoadInfoData(WorkerLoadInfoData&& aOther) = default; + + WorkerLoadInfoData& operator=(WorkerLoadInfoData&& aOther) = default; +}; + +struct WorkerLoadInfo : WorkerLoadInfoData { + WorkerLoadInfo(); + WorkerLoadInfo(WorkerLoadInfo&& aOther) noexcept; + ~WorkerLoadInfo(); + + WorkerLoadInfo& operator=(WorkerLoadInfo&& aOther) = default; + + nsresult SetPrincipalsAndCSPOnMainThread(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal, + nsILoadGroup* aLoadGroup, + nsIContentSecurityPolicy* aCSP); + + nsresult GetPrincipalsAndLoadGroupFromChannel( + nsIChannel* aChannel, nsIPrincipal** aPrincipalOut, + nsIPrincipal** aPartitionedPrincipalOut, nsILoadGroup** aLoadGroupOut); + + nsresult SetPrincipalsAndCSPFromChannel(nsIChannel* aChannel); + + bool FinalChannelPrincipalIsValid(nsIChannel* aChannel); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + bool PrincipalIsValid() const; + + bool PrincipalURIMatchesScriptURL(); +#endif + + bool ProxyReleaseMainThreadObjects(WorkerPrivate* aWorkerPrivate); + + bool ProxyReleaseMainThreadObjects( + WorkerPrivate* aWorkerPrivate, + nsCOMPtr<nsILoadGroup>&& aLoadGroupToCancel); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_WorkerLoadInfo_h diff --git a/dom/workers/WorkerLocation.cpp b/dom/workers/WorkerLocation.cpp new file mode 100644 index 0000000000..94b662efb1 --- /dev/null +++ b/dom/workers/WorkerLocation.cpp @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WorkerLocation.h" + +#include "mozilla/dom/WorkerLocationBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(WorkerLocation) + +/* static */ +already_AddRefed<WorkerLocation> WorkerLocation::Create( + WorkerPrivate::LocationInfo& aInfo) { + RefPtr<WorkerLocation> location = + new WorkerLocation(NS_ConvertUTF8toUTF16(aInfo.mHref), + NS_ConvertUTF8toUTF16(aInfo.mProtocol), + NS_ConvertUTF8toUTF16(aInfo.mHost), + NS_ConvertUTF8toUTF16(aInfo.mHostname), + NS_ConvertUTF8toUTF16(aInfo.mPort), + NS_ConvertUTF8toUTF16(aInfo.mPathname), + NS_ConvertUTF8toUTF16(aInfo.mSearch), + NS_ConvertUTF8toUTF16(aInfo.mHash), aInfo.mOrigin); + + return location.forget(); +} + +JSObject* WorkerLocation::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return WorkerLocation_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerLocation.h b/dom/workers/WorkerLocation.h new file mode 100644 index 0000000000..d51e73cde5 --- /dev/null +++ b/dom/workers/WorkerLocation.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_location_h__ +#define mozilla_dom_location_h__ + +#include "WorkerCommon.h" +#include "WorkerPrivate.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class WorkerLocation final : public nsWrapperCache { + nsString mHref; + nsString mProtocol; + nsString mHost; + nsString mHostname; + nsString mPort; + nsString mPathname; + nsString mSearch; + nsString mHash; + nsString mOrigin; + + WorkerLocation(const nsAString& aHref, const nsAString& aProtocol, + const nsAString& aHost, const nsAString& aHostname, + const nsAString& aPort, const nsAString& aPathname, + const nsAString& aSearch, const nsAString& aHash, + const nsAString& aOrigin) + : mHref(aHref), + mProtocol(aProtocol), + mHost(aHost), + mHostname(aHostname), + mPort(aPort), + mPathname(aPathname), + mSearch(aSearch), + mHash(aHash), + mOrigin(aOrigin) { + MOZ_COUNT_CTOR(WorkerLocation); + } + + MOZ_COUNTED_DTOR(WorkerLocation) + + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(WorkerLocation) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(WorkerLocation) + + static already_AddRefed<WorkerLocation> Create( + WorkerPrivate::LocationInfo& aInfo); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { return nullptr; } + + void Stringify(nsString& aHref) const { aHref = mHref; } + void GetHref(nsString& aHref) const { aHref = mHref; } + void GetProtocol(nsString& aProtocol) const { aProtocol = mProtocol; } + void GetHost(nsString& aHost) const { aHost = mHost; } + void GetHostname(nsString& aHostname) const { aHostname = mHostname; } + void GetPort(nsString& aPort) const { aPort = mPort; } + void GetPathname(nsString& aPathname) const { aPathname = mPathname; } + void GetSearch(nsString& aSearch) const { aSearch = mSearch; } + void GetHash(nsString& aHash) const { aHash = mHash; } + void GetOrigin(nsString& aOrigin) const { aOrigin = mOrigin; } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_location_h__ diff --git a/dom/workers/WorkerNavigator.cpp b/dom/workers/WorkerNavigator.cpp new file mode 100644 index 0000000000..1be36bc7d4 --- /dev/null +++ b/dom/workers/WorkerNavigator.cpp @@ -0,0 +1,285 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WorkerNavigator.h" + +#include <utility> + +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "RuntimeService.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "mozilla/dom/LockManager.h" +#include "mozilla/dom/MediaCapabilities.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/StorageManager.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerNavigatorBinding.h" +#include "mozilla/dom/WorkerStatus.h" +#include "mozilla/dom/network/Connection.h" +#include "mozilla/webgpu/Instance.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIGlobalObject.h" +#include "nsLiteralString.h" +#include "nsPIDOMWindow.h" +#include "nsRFPService.h" +#include "nsString.h" + +class JSObject; +struct JSContext; + +namespace mozilla::dom { + +using namespace workerinternals; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WorkerNavigator) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WorkerNavigator) + tmp->Invalidate(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WorkerNavigator) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStorageManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConnection) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaCapabilities) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWebGpu) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLocks) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +WorkerNavigator::WorkerNavigator(const NavigatorProperties& aProperties, + bool aOnline) + : mProperties(aProperties), mOnline(aOnline) {} + +WorkerNavigator::~WorkerNavigator() { Invalidate(); } + +/* static */ +already_AddRefed<WorkerNavigator> WorkerNavigator::Create(bool aOnLine) { + RuntimeService* rts = RuntimeService::GetService(); + MOZ_ASSERT(rts); + + const RuntimeService::NavigatorProperties& properties = + rts->GetNavigatorProperties(); + + RefPtr<WorkerNavigator> navigator = new WorkerNavigator(properties, aOnLine); + + return navigator.forget(); +} + +void WorkerNavigator::Invalidate() { + if (mStorageManager) { + mStorageManager->Shutdown(); + mStorageManager = nullptr; + } + + mConnection = nullptr; + + mMediaCapabilities = nullptr; + + mWebGpu = nullptr; + + mLocks = nullptr; +} + +JSObject* WorkerNavigator::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return WorkerNavigator_Binding::Wrap(aCx, this, aGivenProto); +} + +bool WorkerNavigator::GlobalPrivacyControl() const { + bool gpcStatus = StaticPrefs::privacy_globalprivacycontrol_enabled(); + if (!gpcStatus) { + JSObject* jso = GetWrapper(); + if (const nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(jso)) { + if (const nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull()) { + gpcStatus = principal->GetPrivateBrowsingId() > 0 && + StaticPrefs::privacy_globalprivacycontrol_pbmode_enabled(); + } + } + } + return StaticPrefs::privacy_globalprivacycontrol_functionality_enabled() && + gpcStatus; +} + +void WorkerNavigator::SetLanguages(const nsTArray<nsString>& aLanguages) { + WorkerNavigator_Binding::ClearCachedLanguagesValue(this); + mProperties.mLanguages = aLanguages.Clone(); +} + +void WorkerNavigator::GetAppVersion(nsString& aAppVersion, + CallerType aCallerType, + ErrorResult& aRv) const { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + if (aCallerType != CallerType::System) { + if (workerPrivate->ShouldResistFingerprinting( + RFPTarget::NavigatorAppVersion)) { + // See nsRFPService.h for spoofed value. + aAppVersion.AssignLiteral(SPOOFED_APPVERSION); + return; + } + + if (!mProperties.mAppVersionOverridden.IsEmpty()) { + aAppVersion = mProperties.mAppVersionOverridden; + return; + } + } + + aAppVersion = mProperties.mAppVersion; +} + +void WorkerNavigator::GetPlatform(nsString& aPlatform, CallerType aCallerType, + ErrorResult& aRv) const { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + if (aCallerType != CallerType::System) { + if (workerPrivate->ShouldResistFingerprinting( + RFPTarget::NavigatorPlatform)) { + // See nsRFPService.h for spoofed value. + aPlatform.AssignLiteral(SPOOFED_PLATFORM); + return; + } + + if (!mProperties.mPlatformOverridden.IsEmpty()) { + aPlatform = mProperties.mPlatformOverridden; + return; + } + } + + aPlatform = mProperties.mPlatform; +} + +namespace { + +/* + * This Worker Runnable needs to check RFP; but our standard way of doing so + * relies on accessing GlobalScope() - which can only be accessed on the worker + * thread. So we need to pass it in. + */ +class GetUserAgentRunnable final : public WorkerMainThreadRunnable { + nsString& mUA; + bool mShouldResistFingerprinting; + + public: + GetUserAgentRunnable(WorkerPrivate* aWorkerPrivate, nsString& aUA, + bool aShouldResistFingerprinting) + : WorkerMainThreadRunnable(aWorkerPrivate, "UserAgent getter"_ns), + mUA(aUA), + mShouldResistFingerprinting(aShouldResistFingerprinting) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + virtual bool MainThreadRun() override { + AssertIsOnMainThread(); + + nsCOMPtr<nsPIDOMWindowInner> window = mWorkerPrivate->GetWindow(); + + nsresult rv = + dom::Navigator::GetUserAgent(window, mWorkerPrivate->GetDocument(), + Some(mShouldResistFingerprinting), mUA); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to retrieve user-agent from the worker thread."); + } + + return true; + } +}; + +} // namespace + +void WorkerNavigator::GetUserAgent(nsString& aUserAgent, CallerType aCallerType, + ErrorResult& aRv) const { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<GetUserAgentRunnable> runnable = new GetUserAgentRunnable( + workerPrivate, aUserAgent, + workerPrivate->ShouldResistFingerprinting(RFPTarget::NavigatorUserAgent)); + + runnable->Dispatch(Canceling, aRv); +} + +uint64_t WorkerNavigator::HardwareConcurrency() const { + RuntimeService* rts = RuntimeService::GetService(); + MOZ_ASSERT(rts); + + WorkerPrivate* aWorkerPrivate = GetCurrentThreadWorkerPrivate(); + bool rfp = aWorkerPrivate->ShouldResistFingerprinting( + RFPTarget::NavigatorHWConcurrency); + + return rts->ClampedHardwareConcurrency(rfp); +} + +StorageManager* WorkerNavigator::Storage() { + if (!mStorageManager) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<nsIGlobalObject> global = workerPrivate->GlobalScope(); + MOZ_ASSERT(global); + + mStorageManager = new StorageManager(global); + } + + return mStorageManager; +} + +network::Connection* WorkerNavigator::GetConnection(ErrorResult& aRv) { + if (!mConnection) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + mConnection = network::Connection::CreateForWorker(workerPrivate, aRv); + } + + return mConnection; +} + +dom::MediaCapabilities* WorkerNavigator::MediaCapabilities() { + if (!mMediaCapabilities) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + nsIGlobalObject* global = workerPrivate->GlobalScope(); + MOZ_ASSERT(global); + + mMediaCapabilities = new dom::MediaCapabilities(global); + } + return mMediaCapabilities; +} + +webgpu::Instance* WorkerNavigator::Gpu() { + if (!mWebGpu) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + nsIGlobalObject* global = workerPrivate->GlobalScope(); + MOZ_ASSERT(global); + + mWebGpu = webgpu::Instance::Create(global); + } + return mWebGpu; +} + +dom::LockManager* WorkerNavigator::Locks() { + if (!mLocks) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + nsIGlobalObject* global = workerPrivate->GlobalScope(); + MOZ_ASSERT(global); + + mLocks = dom::LockManager::Create(*global); + } + return mLocks; +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerNavigator.h b/dom/workers/WorkerNavigator.h new file mode 100644 index 0000000000..0db9292d78 --- /dev/null +++ b/dom/workers/WorkerNavigator.h @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workernavigator_h__ +#define mozilla_dom_workernavigator_h__ + +#include <stdint.h> +#include "js/RootingAPI.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/workerinternals/RuntimeService.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace webgpu { +class Instance; +} // namespace webgpu +namespace dom { +class StorageManager; +class MediaCapabilities; +class LockManager; + +namespace network { +class Connection; +} // namespace network + +class WorkerNavigator final : public nsWrapperCache { + using NavigatorProperties = + workerinternals::RuntimeService::NavigatorProperties; + + NavigatorProperties mProperties; + RefPtr<StorageManager> mStorageManager; + RefPtr<network::Connection> mConnection; + RefPtr<dom::MediaCapabilities> mMediaCapabilities; + RefPtr<webgpu::Instance> mWebGpu; + RefPtr<dom::LockManager> mLocks; + bool mOnline; + + WorkerNavigator(const NavigatorProperties& aProperties, bool aOnline); + ~WorkerNavigator(); + + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(WorkerNavigator) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(WorkerNavigator) + + static already_AddRefed<WorkerNavigator> Create(bool aOnLine); + + void Invalidate(); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { return nullptr; } + + void GetAppCodeName(nsString& aAppCodeName, ErrorResult& /* unused */) const { + aAppCodeName.AssignLiteral("Mozilla"); + } + void GetAppName(nsString& aAppName) const { + aAppName.AssignLiteral("Netscape"); + } + + void GetAppVersion(nsString& aAppVersion, CallerType aCallerType, + ErrorResult& aRv) const; + + void GetPlatform(nsString& aPlatform, CallerType aCallerType, + ErrorResult& aRv) const; + + void GetProduct(nsString& aProduct) const { aProduct.AssignLiteral("Gecko"); } + + bool TaintEnabled() const { return false; } + + void GetLanguage(nsString& aLanguage) const { + MOZ_ASSERT(mProperties.mLanguages.Length() >= 1); + aLanguage.Assign(mProperties.mLanguages[0]); + } + + void GetLanguages(nsTArray<nsString>& aLanguages) const { + aLanguages = mProperties.mLanguages.Clone(); + } + + void GetUserAgent(nsString& aUserAgent, CallerType aCallerType, + ErrorResult& aRv) const; + + bool OnLine() const { return mOnline; } + + // Worker thread only! + void SetOnLine(bool aOnline) { mOnline = aOnline; } + + bool GlobalPrivacyControl() const; + + void SetLanguages(const nsTArray<nsString>& aLanguages); + + uint64_t HardwareConcurrency() const; + + StorageManager* Storage(); + + network::Connection* GetConnection(ErrorResult& aRv); + + dom::MediaCapabilities* MediaCapabilities(); + + webgpu::Instance* Gpu(); + + dom::LockManager* Locks(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workernavigator_h__ diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp new file mode 100644 index 0000000000..78de4adc3a --- /dev/null +++ b/dom/workers/WorkerPrivate.cpp @@ -0,0 +1,6208 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerPrivate.h" + +#include <utility> + +#include "js/CallAndConstruct.h" // JS_CallFunctionValue +#include "js/CompilationAndEvaluation.h" +#include "js/ContextOptions.h" +#include "js/Exception.h" +#include "js/friend/ErrorMessages.h" // JSMSG_OUT_OF_MEMORY +#include "js/LocaleSensitive.h" +#include "js/MemoryMetrics.h" +#include "js/SourceText.h" +#include "MessageEventRunnable.h" +#include "mozilla/AntiTrackingUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/Mutex.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/Result.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/CallbackDebuggerNotification.h" +#include "mozilla/dom/ClientManager.h" +#include "mozilla/dom/ClientState.h" +#include "mozilla/dom/Console.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/MessagePortBinding.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/PerformanceStorageWorker.h" +#include "mozilla/dom/PromiseDebugging.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/RemoteWorkerService.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/TimeoutHandler.h" +#include "mozilla/dom/UseCounterMetrics.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/WorkerStatus.h" +#include "mozilla/dom/WebTaskScheduler.h" +#include "mozilla/dom/JSExecutionManager.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/extensions/ExtensionBrowser.h" // extensions::Create{AndDispatchInitWorkerContext,WorkerLoaded,WorkerDestroyed}Runnable +#include "mozilla/extensions/WebExtensionPolicy.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/Telemetry.h" +#include "mozilla/ThreadEventQueue.h" +#include "mozilla/ThreadSafety.h" +#include "mozilla/ThrottledEventQueue.h" +#include "nsCycleCollector.h" +#include "nsGlobalWindowInner.h" +#include "nsIDUtils.h" +#include "nsNetUtil.h" +#include "nsIFile.h" +#include "nsIMemoryReporter.h" +#include "nsIPermissionManager.h" +#include "nsIProtocolHandler.h" +#include "nsIScriptError.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIUUIDGenerator.h" +#include "nsPrintfCString.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsRFPService.h" +#include "nsSandboxFlags.h" +#include "nsThreadUtils.h" +#include "nsUTF8Utils.h" + +#include "RuntimeService.h" +#include "ScriptLoader.h" +#include "mozilla/dom/ServiceWorkerEvents.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/net/CookieJarSettings.h" +#include "WorkerCSPEventListener.h" +#include "WorkerDebugger.h" +#include "WorkerDebuggerManager.h" +#include "WorkerError.h" +#include "WorkerEventTarget.h" +#include "WorkerNavigator.h" +#include "WorkerRef.h" +#include "WorkerRunnable.h" +#include "WorkerThread.h" +#include "nsContentSecurityManager.h" + +#include "nsThreadManager.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +// JS_MaybeGC will run once every second during normal execution. +#define PERIODIC_GC_TIMER_DELAY_SEC 1 + +// A shrinking GC will run five seconds after the last event is processed. +#define IDLE_GC_TIMER_DELAY_SEC 5 + +static mozilla::LazyLogModule sWorkerPrivateLog("WorkerPrivate"); +static mozilla::LazyLogModule sWorkerTimeoutsLog("WorkerTimeouts"); + +mozilla::LogModule* WorkerLog() { return sWorkerPrivateLog; } + +mozilla::LogModule* TimeoutsLog() { return sWorkerTimeoutsLog; } + +#ifdef LOG +# undef LOG +#endif +#ifdef LOGV +# undef LOGV +#endif +#define LOG(log, _args) MOZ_LOG(log, LogLevel::Debug, _args); +#define LOGV(args) MOZ_LOG(sWorkerPrivateLog, LogLevel::Verbose, args); + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +using namespace workerinternals; + +MOZ_DEFINE_MALLOC_SIZE_OF(JsWorkerMallocSizeOf) + +namespace { + +#ifdef DEBUG + +const nsIID kDEBUGWorkerEventTargetIID = { + 0xccaba3fa, + 0x5be2, + 0x4de2, + {0xba, 0x87, 0x3b, 0x3b, 0x5b, 0x1d, 0x5, 0xfb}}; + +#endif + +template <class T> +class UniquePtrComparator { + using A = UniquePtr<T>; + using B = T*; + + public: + bool Equals(const A& a, const A& b) const { + return (a && b) ? (*a == *b) : (!a && !b); + } + bool LessThan(const A& a, const A& b) const { + return (a && b) ? (*a < *b) : !!b; + } +}; + +template <class T> +inline UniquePtrComparator<T> GetUniquePtrComparator( + const nsTArray<UniquePtr<T>>&) { + return UniquePtrComparator<T>(); +} + +// This class is used to wrap any runnables that the worker receives via the +// nsIEventTarget::Dispatch() method (either from NS_DispatchToCurrentThread or +// from the worker's EventTarget). +class ExternalRunnableWrapper final : public WorkerRunnable { + nsCOMPtr<nsIRunnable> mWrappedRunnable; + + public: + ExternalRunnableWrapper(WorkerPrivate* aWorkerPrivate, + nsIRunnable* aWrappedRunnable) + : WorkerRunnable(aWorkerPrivate, "ExternalRunnableWrapper", WorkerThread), + mWrappedRunnable(aWrappedRunnable) { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWrappedRunnable); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ExternalRunnableWrapper, WorkerRunnable) + + private: + ~ExternalRunnableWrapper() = default; + + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + // Silence bad assertions. + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // Silence bad assertions. + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + nsresult rv = mWrappedRunnable->Run(); + mWrappedRunnable = nullptr; + if (NS_FAILED(rv)) { + if (!JS_IsExceptionPending(aCx)) { + Throw(aCx, rv); + } + return false; + } + return true; + } + + nsresult Cancel() override { + nsCOMPtr<nsIDiscardableRunnable> doomed = + do_QueryInterface(mWrappedRunnable); + if (doomed) { + doomed->OnDiscard(); + } + mWrappedRunnable = nullptr; + return NS_OK; + } +}; + +struct WindowAction { + nsPIDOMWindowInner* mWindow; + bool mDefaultAction; + + MOZ_IMPLICIT WindowAction(nsPIDOMWindowInner* aWindow) + : mWindow(aWindow), mDefaultAction(true) {} + + bool operator==(const WindowAction& aOther) const { + return mWindow == aOther.mWindow; + } +}; + +class WorkerFinishedRunnable final : public WorkerControlRunnable { + WorkerPrivate* mFinishedWorker; + + public: + WorkerFinishedRunnable(WorkerPrivate* aWorkerPrivate, + WorkerPrivate* aFinishedWorker) + : WorkerControlRunnable(aWorkerPrivate, "WorkerFinishedRunnable", + WorkerThread), + mFinishedWorker(aFinishedWorker) { + aFinishedWorker->IncreaseWorkerFinishedRunnableCount(); + } + + private: + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + // Silence bad assertions. + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // Silence bad assertions. + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + // This may block on the main thread. + AutoYieldJSThreadExecution yield; + + mFinishedWorker->DecreaseWorkerFinishedRunnableCount(); + + if (!mFinishedWorker->ProxyReleaseMainThreadObjects()) { + NS_WARNING("Failed to dispatch, going to leak!"); + } + + RuntimeService* runtime = RuntimeService::GetService(); + NS_ASSERTION(runtime, "This should never be null!"); + + mFinishedWorker->DisableDebugger(); + + runtime->UnregisterWorker(*mFinishedWorker); + + mFinishedWorker->ClearSelfAndParentEventTargetRef(); + return true; + } +}; + +class TopLevelWorkerFinishedRunnable final : public Runnable { + WorkerPrivate* mFinishedWorker; + + public: + explicit TopLevelWorkerFinishedRunnable(WorkerPrivate* aFinishedWorker) + : mozilla::Runnable("TopLevelWorkerFinishedRunnable"), + mFinishedWorker(aFinishedWorker) { + aFinishedWorker->AssertIsOnWorkerThread(); + aFinishedWorker->IncreaseTopLevelWorkerFinishedRunnableCount(); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(TopLevelWorkerFinishedRunnable, Runnable) + + private: + ~TopLevelWorkerFinishedRunnable() = default; + + NS_IMETHOD + Run() override { + AssertIsOnMainThread(); + + mFinishedWorker->DecreaseTopLevelWorkerFinishedRunnableCount(); + + RuntimeService* runtime = RuntimeService::GetService(); + MOZ_ASSERT(runtime); + + mFinishedWorker->DisableDebugger(); + + runtime->UnregisterWorker(*mFinishedWorker); + + if (!mFinishedWorker->ProxyReleaseMainThreadObjects()) { + NS_WARNING("Failed to dispatch, going to leak!"); + } + + mFinishedWorker->ClearSelfAndParentEventTargetRef(); + return NS_OK; + } +}; + +class CompileScriptRunnable final : public WorkerDebuggeeRunnable { + nsString mScriptURL; + const mozilla::Encoding* mDocumentEncoding; + UniquePtr<SerializedStackHolder> mOriginStack; + + public: + explicit CompileScriptRunnable(WorkerPrivate* aWorkerPrivate, + UniquePtr<SerializedStackHolder> aOriginStack, + const nsAString& aScriptURL, + const mozilla::Encoding* aDocumentEncoding) + : WorkerDebuggeeRunnable(aWorkerPrivate, "CompileScriptRunnable", + WorkerThread), + mScriptURL(aScriptURL), + mDocumentEncoding(aDocumentEncoding), + mOriginStack(aOriginStack.release()) {} + + private: + // We can't implement PreRun effectively, because at the point when that would + // run we have not yet done our load so don't know things like our final + // principal and whatnot. + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + + WorkerGlobalScope* globalScope = + aWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (NS_WARN_IF(!globalScope)) { + return false; + } + + if (NS_WARN_IF(!aWorkerPrivate->EnsureCSPEventListener())) { + return false; + } + + ErrorResult rv; + workerinternals::LoadMainScript(aWorkerPrivate, std::move(mOriginStack), + mScriptURL, WorkerScript, rv, + mDocumentEncoding); + + if (aWorkerPrivate->ExtensionAPIAllowed()) { + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + RefPtr<Runnable> extWorkerRunnable = + extensions::CreateWorkerLoadedRunnable( + aWorkerPrivate->ServiceWorkerID(), aWorkerPrivate->GetBaseURI()); + // Dispatch as a low priority runnable. + if (NS_FAILED(aWorkerPrivate->DispatchToMainThreadForMessaging( + extWorkerRunnable.forget()))) { + NS_WARNING( + "Failed to dispatch runnable to notify extensions worker loaded"); + } + } + + rv.WouldReportJSException(); + // Explicitly ignore NS_BINDING_ABORTED on rv. Or more precisely, still + // return false and don't SetWorkerScriptExecutedSuccessfully() in that + // case, but don't throw anything on aCx. The idea is to not dispatch error + // events if our load is canceled with that error code. + if (rv.ErrorCodeIs(NS_BINDING_ABORTED)) { + rv.SuppressException(); + return false; + } + + // Make sure to propagate exceptions from rv onto aCx, so that they will get + // reported after we return. We want to propagate just JS exceptions, + // because all the other errors are handled when the script is loaded. + // See: https://dom.spec.whatwg.org/#concept-event-fire + if (rv.Failed() && !rv.IsJSException()) { + WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent( + aWorkerPrivate); + rv.SuppressException(); + return false; + } + + // This is a little dumb, but aCx is in the null realm here because we + // set it up that way in our Run(), since we had not created the global at + // that point yet. So we need to enter the realm of our global, + // because setting a pending exception on aCx involves wrapping into its + // current compartment. Luckily we have a global now. + JSAutoRealm ar(aCx, globalScope->GetGlobalJSObject()); + if (rv.MaybeSetPendingException(aCx)) { + // In the event of an uncaught exception, the worker should still keep + // running (return true) but should not be marked as having executed + // successfully (which will cause ServiceWorker installation to fail). + // In previous error handling cases in this method, we return false (to + // trigger CloseInternal) because the global is not in an operable + // state at all. + // + // For ServiceWorkers, this would correspond to the "Run Service Worker" + // algorithm returning an "abrupt completion" and _not_ failure. + // + // For DedicatedWorkers and SharedWorkers, this would correspond to the + // "run a worker" algorithm disregarding the return value of "run the + // classic script"/"run the module script" in step 24: + // + // "If script is a classic script, then run the classic script script. + // Otherwise, it is a module script; run the module script script." + return true; + } + + aWorkerPrivate->SetWorkerScriptExecutedSuccessfully(); + return true; + } + + void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult) override { + if (!aRunResult) { + aWorkerPrivate->CloseInternal(); + } + WorkerRunnable::PostRun(aCx, aWorkerPrivate, aRunResult); + } +}; + +class NotifyRunnable final : public WorkerControlRunnable { + WorkerStatus mStatus; + + public: + NotifyRunnable(WorkerPrivate* aWorkerPrivate, WorkerStatus aStatus) + : WorkerControlRunnable(aWorkerPrivate, "NotifyRunnable", WorkerThread), + mStatus(aStatus) { + MOZ_ASSERT(aStatus == Closing || aStatus == Canceling || + aStatus == Killing); + } + + private: + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnParentThread(); + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + aWorkerPrivate->AssertIsOnParentThread(); + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + return aWorkerPrivate->NotifyInternal(mStatus); + } + + virtual void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult) override {} +}; + +class FreezeRunnable final : public WorkerControlRunnable { + public: + explicit FreezeRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, "FreezeRunnable", WorkerThread) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + return aWorkerPrivate->FreezeInternal(); + } +}; + +class ThawRunnable final : public WorkerControlRunnable { + public: + explicit ThawRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, "ThawRunnable", WorkerThread) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + return aWorkerPrivate->ThawInternal(); + } +}; + +class PropagateStorageAccessPermissionGrantedRunnable final + : public WorkerControlRunnable { + public: + explicit PropagateStorageAccessPermissionGrantedRunnable( + WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, + "PropagateStorageAccessPermissionGrantedRunnable", + WorkerThread) {} + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->PropagateStorageAccessPermissionGrantedInternal(); + return true; + } +}; + +class ReportErrorToConsoleRunnable final : public WorkerRunnable { + const char* mMessage; + const nsTArray<nsString> mParams; + + public: + // aWorkerPrivate is the worker thread we're on (or the main thread, if null) + static void Report(WorkerPrivate* aWorkerPrivate, const char* aMessage, + const nsTArray<nsString>& aParams) { + if (aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + } else { + AssertIsOnMainThread(); + } + + // Now fire a runnable to do the same on the parent's thread if we can. + if (aWorkerPrivate) { + RefPtr<ReportErrorToConsoleRunnable> runnable = + new ReportErrorToConsoleRunnable(aWorkerPrivate, aMessage, aParams); + runnable->Dispatch(); + return; + } + + // Log a warning to the console. + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + nullptr, nsContentUtils::eDOM_PROPERTIES, + aMessage, aParams); + } + + private: + ReportErrorToConsoleRunnable(WorkerPrivate* aWorkerPrivate, + const char* aMessage, + const nsTArray<nsString>& aParams) + : WorkerRunnable(aWorkerPrivate, "ReportErrorToConsoleRunnable", + ParentThread), + mMessage(aMessage), + mParams(aParams.Clone()) {} + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // Dispatch may fail if the worker was canceled, no need to report that as + // an error, so don't call base class PostDispatch. + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + WorkerPrivate* parent = aWorkerPrivate->GetParent(); + MOZ_ASSERT_IF(!parent, NS_IsMainThread()); + Report(parent, mMessage, mParams); + return true; + } +}; + +class RunExpiredTimoutsRunnable final : public WorkerRunnable, + public nsITimerCallback { + public: + NS_DECL_ISUPPORTS_INHERITED + + explicit RunExpiredTimoutsRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerRunnable(aWorkerPrivate, "RunExpiredTimoutsRunnable", + WorkerThread) {} + + private: + ~RunExpiredTimoutsRunnable() = default; + + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + // Silence bad assertions. + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // Silence bad assertions. + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until worker runnables are generally + // MOZ_CAN_RUN_SCRIPT. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + return aWorkerPrivate->RunExpiredTimeouts(aCx); + } + + NS_IMETHOD + Notify(nsITimer* aTimer) override { return Run(); } +}; + +NS_IMPL_ISUPPORTS_INHERITED(RunExpiredTimoutsRunnable, WorkerRunnable, + nsITimerCallback) + +class DebuggerImmediateRunnable final : public WorkerRunnable { + RefPtr<dom::Function> mHandler; + + public: + explicit DebuggerImmediateRunnable(WorkerPrivate* aWorkerPrivate, + dom::Function& aHandler) + : WorkerRunnable(aWorkerPrivate, "DebuggerImmediateRunnable", + WorkerThread), + mHandler(&aHandler) {} + + private: + virtual bool IsDebuggerRunnable() const override { return true; } + + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + // Silence bad assertions. + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // Silence bad assertions. + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + JS::Rooted<JS::Value> callable( + aCx, JS::ObjectOrNullValue(mHandler->CallableOrNull())); + JS::HandleValueArray args = JS::HandleValueArray::empty(); + JS::Rooted<JS::Value> rval(aCx); + + // WorkerRunnable::Run will report the exception if it happens. + return JS_CallFunctionValue(aCx, global, callable, args, &rval); + } +}; + +// GetJSContext() is safe on the worker thread +void PeriodicGCTimerCallback(nsITimer* aTimer, + void* aClosure) MOZ_NO_THREAD_SAFETY_ANALYSIS { + auto* workerPrivate = static_cast<WorkerPrivate*>(aClosure); + MOZ_DIAGNOSTIC_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + workerPrivate->GarbageCollectInternal(workerPrivate->GetJSContext(), + false /* shrinking */, + false /* collect children */); + LOG(WorkerLog(), ("Worker %p run periodic GC\n", workerPrivate)); +} + +void IdleGCTimerCallback(nsITimer* aTimer, + void* aClosure) MOZ_NO_THREAD_SAFETY_ANALYSIS { + auto* workerPrivate = static_cast<WorkerPrivate*>(aClosure); + MOZ_DIAGNOSTIC_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + workerPrivate->GarbageCollectInternal(workerPrivate->GetJSContext(), + true /* shrinking */, + false /* collect children */); + LOG(WorkerLog(), ("Worker %p run idle GC\n", workerPrivate)); + + // After running idle GC we can cancel the current timers. + workerPrivate->CancelGCTimers(); +} + +class UpdateContextOptionsRunnable final : public WorkerControlRunnable { + JS::ContextOptions mContextOptions; + + public: + UpdateContextOptionsRunnable(WorkerPrivate* aWorkerPrivate, + const JS::ContextOptions& aContextOptions) + : WorkerControlRunnable(aWorkerPrivate, "UpdateContextOptionsRunnable", + WorkerThread), + mContextOptions(aContextOptions) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->UpdateContextOptionsInternal(aCx, mContextOptions); + return true; + } +}; + +class UpdateLanguagesRunnable final : public WorkerRunnable { + nsTArray<nsString> mLanguages; + + public: + UpdateLanguagesRunnable(WorkerPrivate* aWorkerPrivate, + const nsTArray<nsString>& aLanguages) + : WorkerRunnable(aWorkerPrivate, "UpdateLanguagesRunnable"), + mLanguages(aLanguages.Clone()) {} + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->UpdateLanguagesInternal(mLanguages); + return true; + } +}; + +class UpdateJSWorkerMemoryParameterRunnable final + : public WorkerControlRunnable { + Maybe<uint32_t> mValue; + JSGCParamKey mKey; + + public: + UpdateJSWorkerMemoryParameterRunnable(WorkerPrivate* aWorkerPrivate, + JSGCParamKey aKey, + Maybe<uint32_t> aValue) + : WorkerControlRunnable(aWorkerPrivate, + "UpdateJSWorkerMemoryParameterRunnable", + WorkerThread), + mValue(aValue), + mKey(aKey) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->UpdateJSWorkerMemoryParameterInternal(aCx, mKey, mValue); + return true; + } +}; + +#ifdef JS_GC_ZEAL +class UpdateGCZealRunnable final : public WorkerControlRunnable { + uint8_t mGCZeal; + uint32_t mFrequency; + + public: + UpdateGCZealRunnable(WorkerPrivate* aWorkerPrivate, uint8_t aGCZeal, + uint32_t aFrequency) + : WorkerControlRunnable(aWorkerPrivate, "UpdateGCZealRunnable", + WorkerThread), + mGCZeal(aGCZeal), + mFrequency(aFrequency) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->UpdateGCZealInternal(aCx, mGCZeal, mFrequency); + return true; + } +}; +#endif + +class SetLowMemoryStateRunnable final : public WorkerControlRunnable { + bool mState; + + public: + SetLowMemoryStateRunnable(WorkerPrivate* aWorkerPrivate, bool aState) + : WorkerControlRunnable(aWorkerPrivate, "SetLowMemoryStateRunnable", + WorkerThread), + mState(aState) {} + + private: + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->SetLowMemoryStateInternal(aCx, mState); + return true; + } +}; + +class GarbageCollectRunnable final : public WorkerControlRunnable { + bool mShrinking; + bool mCollectChildren; + + public: + GarbageCollectRunnable(WorkerPrivate* aWorkerPrivate, bool aShrinking, + bool aCollectChildren) + : WorkerControlRunnable(aWorkerPrivate, "GarbageCollectRunnable", + WorkerThread), + mShrinking(aShrinking), + mCollectChildren(aCollectChildren) {} + + private: + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + // Silence bad assertions, this can be dispatched from either the main + // thread or the timer thread.. + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + // Silence bad assertions, this can be dispatched from either the main + // thread or the timer thread.. + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->GarbageCollectInternal(aCx, mShrinking, mCollectChildren); + if (mShrinking) { + // Either we've run the idle GC or explicit GC call from the parent, + // we can cancel the current timers. + aWorkerPrivate->CancelGCTimers(); + } + return true; + } +}; + +class CycleCollectRunnable final : public WorkerControlRunnable { + bool mCollectChildren; + + public: + CycleCollectRunnable(WorkerPrivate* aWorkerPrivate, bool aCollectChildren) + : WorkerControlRunnable(aWorkerPrivate, "CycleCollectRunnable", + WorkerThread), + mCollectChildren(aCollectChildren) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->CycleCollectInternal(mCollectChildren); + return true; + } +}; + +class OfflineStatusChangeRunnable final : public WorkerRunnable { + public: + OfflineStatusChangeRunnable(WorkerPrivate* aWorkerPrivate, bool aIsOffline) + : WorkerRunnable(aWorkerPrivate, "OfflineStatusChangeRunnable"), + mIsOffline(aIsOffline) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->OfflineStatusChangeEventInternal(mIsOffline); + return true; + } + + private: + bool mIsOffline; +}; + +class MemoryPressureRunnable final : public WorkerControlRunnable { + public: + explicit MemoryPressureRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, "MemoryPressureRunnable", + WorkerThread) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->MemoryPressureInternal(); + return true; + } +}; + +#ifdef DEBUG +static bool StartsWithExplicit(nsACString& s) { + return StringBeginsWith(s, "explicit/"_ns); +} +#endif + +PRThread* PRThreadFromThread(nsIThread* aThread) { + MOZ_ASSERT(aThread); + + PRThread* result; + MOZ_ALWAYS_SUCCEEDS(aThread->GetPRThread(&result)); + MOZ_ASSERT(result); + + return result; +} + +// A runnable to cancel the worker from the parent thread when self.close() is +// called. This runnable is executed on the parent process in order to cancel +// the current runnable. It uses a normal WorkerDebuggeeRunnable in order to be +// sure that all the pending WorkerDebuggeeRunnables are executed before this. +class CancelingOnParentRunnable final : public WorkerDebuggeeRunnable { + public: + explicit CancelingOnParentRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerDebuggeeRunnable(aWorkerPrivate, "CancelingOnParentRunnable", + ParentThread) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->Cancel(); + return true; + } +}; + +// A runnable to cancel the worker from the parent process. +class CancelingWithTimeoutOnParentRunnable final + : public WorkerControlRunnable { + public: + explicit CancelingWithTimeoutOnParentRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, + "CancelingWithTimeoutOnParentRunnable", + ParentThread) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnParentThread(); + aWorkerPrivate->StartCancelingTimer(); + return true; + } +}; + +class CancelingTimerCallback final : public nsITimerCallback { + public: + NS_DECL_ISUPPORTS + + explicit CancelingTimerCallback(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) {} + + NS_IMETHOD + Notify(nsITimer* aTimer) override { + mWorkerPrivate->AssertIsOnParentThread(); + mWorkerPrivate->Cancel(); + return NS_OK; + } + + private: + ~CancelingTimerCallback() = default; + + // Raw pointer here is OK because the timer is canceled during the shutdown + // steps. + WorkerPrivate* mWorkerPrivate; +}; + +NS_IMPL_ISUPPORTS(CancelingTimerCallback, nsITimerCallback) + +// This runnable starts the canceling of a worker after a self.close(). +class CancelingRunnable final : public Runnable { + public: + CancelingRunnable() : Runnable("CancelingRunnable") {} + + NS_IMETHOD + Run() override { + LOG(WorkerLog(), ("CancelingRunnable::Run [%p]", this)); + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + // Now we can cancel the this worker from the parent process. + RefPtr<CancelingOnParentRunnable> r = + new CancelingOnParentRunnable(workerPrivate); + r->Dispatch(); + + return NS_OK; + } +}; + +} /* anonymous namespace */ + +nsString ComputeWorkerPrivateId() { + nsID uuid = nsID::GenerateUUID(); + return NSID_TrimBracketsUTF16(uuid); +} + +class WorkerPrivate::EventTarget final : public nsISerialEventTarget { + // This mutex protects mWorkerPrivate and must be acquired *before* the + // WorkerPrivate's mutex whenever they must both be held. + mozilla::Mutex mMutex; + WorkerPrivate* mWorkerPrivate MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIEventTarget> mNestedEventTarget MOZ_GUARDED_BY(mMutex); + bool mDisabled MOZ_GUARDED_BY(mMutex); + bool mShutdown MOZ_GUARDED_BY(mMutex); + + public: + EventTarget(WorkerPrivate* aWorkerPrivate, nsIEventTarget* aNestedEventTarget) + : mMutex("WorkerPrivate::EventTarget::mMutex"), + mWorkerPrivate(aWorkerPrivate), + mNestedEventTarget(aNestedEventTarget), + mDisabled(false), + mShutdown(false) { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aNestedEventTarget); + } + + void Disable() { + { + MutexAutoLock lock(mMutex); + + // Note, Disable() can be called more than once safely. + mDisabled = true; + } + } + + void Shutdown() { + nsCOMPtr<nsIEventTarget> nestedEventTarget; + { + MutexAutoLock lock(mMutex); + + mWorkerPrivate = nullptr; + mNestedEventTarget.swap(nestedEventTarget); + MOZ_ASSERT(mDisabled); + mShutdown = true; + } + } + + RefPtr<nsIEventTarget> GetNestedEventTarget() { + RefPtr<nsIEventTarget> nestedEventTarget = nullptr; + { + MutexAutoLock lock(mMutex); + if (mWorkerPrivate) { + mWorkerPrivate->AssertIsOnWorkerThread(); + nestedEventTarget = mNestedEventTarget.get(); + } + } + return nestedEventTarget; + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + + private: + ~EventTarget() = default; +}; + +struct WorkerPrivate::TimeoutInfo { + TimeoutInfo() + : mId(0), + mNestingLevel(0), + mReason(Timeout::Reason::eTimeoutOrInterval), + mIsInterval(false), + mCanceled(false), + mOnChromeWorker(false) { + MOZ_COUNT_CTOR(mozilla::dom::WorkerPrivate::TimeoutInfo); + } + + ~TimeoutInfo() { MOZ_COUNT_DTOR(mozilla::dom::WorkerPrivate::TimeoutInfo); } + + bool operator==(const TimeoutInfo& aOther) const { + return mTargetTime == aOther.mTargetTime; + } + + bool operator<(const TimeoutInfo& aOther) const { + return mTargetTime < aOther.mTargetTime; + } + + void AccumulateNestingLevel(const uint32_t& aBaseLevel) { + if (aBaseLevel < StaticPrefs::dom_clamp_timeout_nesting_level_AtStartup()) { + mNestingLevel = aBaseLevel + 1; + return; + } + mNestingLevel = StaticPrefs::dom_clamp_timeout_nesting_level_AtStartup(); + } + + void CalculateTargetTime() { + auto target = mInterval; + // Don't clamp timeout for chrome workers + if (mNestingLevel >= + StaticPrefs::dom_clamp_timeout_nesting_level_AtStartup() && + !mOnChromeWorker) { + target = TimeDuration::Max( + mInterval, + TimeDuration::FromMilliseconds(StaticPrefs::dom_min_timeout_value())); + } + mTargetTime = TimeStamp::Now() + target; + } + + RefPtr<TimeoutHandler> mHandler; + mozilla::TimeStamp mTargetTime; + mozilla::TimeDuration mInterval; + int32_t mId; + uint32_t mNestingLevel; + Timeout::Reason mReason; + bool mIsInterval; + bool mCanceled; + bool mOnChromeWorker; +}; + +class WorkerJSContextStats final : public JS::RuntimeStats { + const nsCString mRtPath; + + public: + explicit WorkerJSContextStats(const nsACString& aRtPath) + : JS::RuntimeStats(JsWorkerMallocSizeOf), mRtPath(aRtPath) {} + + ~WorkerJSContextStats() { + for (JS::ZoneStats& stats : zoneStatsVector) { + delete static_cast<xpc::ZoneStatsExtras*>(stats.extra); + } + + for (JS::RealmStats& stats : realmStatsVector) { + delete static_cast<xpc::RealmStatsExtras*>(stats.extra); + } + } + + const nsCString& Path() const { return mRtPath; } + + virtual void initExtraZoneStats(JS::Zone* aZone, JS::ZoneStats* aZoneStats, + const JS::AutoRequireNoGC& nogc) override { + MOZ_ASSERT(!aZoneStats->extra); + + // ReportJSRuntimeExplicitTreeStats expects that + // aZoneStats->extra is a xpc::ZoneStatsExtras pointer. + xpc::ZoneStatsExtras* extras = new xpc::ZoneStatsExtras; + extras->pathPrefix = mRtPath; + extras->pathPrefix += nsPrintfCString("zone(0x%p)/", (void*)aZone); + + MOZ_ASSERT(StartsWithExplicit(extras->pathPrefix)); + + aZoneStats->extra = extras; + } + + virtual void initExtraRealmStats(JS::Realm* aRealm, + JS::RealmStats* aRealmStats, + const JS::AutoRequireNoGC& nogc) override { + MOZ_ASSERT(!aRealmStats->extra); + + // ReportJSRuntimeExplicitTreeStats expects that + // aRealmStats->extra is a xpc::RealmStatsExtras pointer. + xpc::RealmStatsExtras* extras = new xpc::RealmStatsExtras; + + // This is the |jsPathPrefix|. Each worker has exactly one realm. + extras->jsPathPrefix.Assign(mRtPath); + extras->jsPathPrefix += + nsPrintfCString("zone(0x%p)/", (void*)js::GetRealmZone(aRealm)); + extras->jsPathPrefix += "realm(web-worker)/"_ns; + + // This should never be used when reporting with workers (hence the "?!"). + extras->domPathPrefix.AssignLiteral("explicit/workers/?!/"); + + MOZ_ASSERT(StartsWithExplicit(extras->jsPathPrefix)); + MOZ_ASSERT(StartsWithExplicit(extras->domPathPrefix)); + + extras->location = nullptr; + + aRealmStats->extra = extras; + } +}; + +class WorkerPrivate::MemoryReporter final : public nsIMemoryReporter { + NS_DECL_THREADSAFE_ISUPPORTS + + friend class WorkerPrivate; + + SharedMutex mMutex; + WorkerPrivate* mWorkerPrivate; + + public: + explicit MemoryReporter(WorkerPrivate* aWorkerPrivate) + : mMutex(aWorkerPrivate->mMutex), mWorkerPrivate(aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + NS_IMETHOD + CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize) override; + + private: + class FinishCollectRunnable; + + class CollectReportsRunnable final : public MainThreadWorkerControlRunnable { + RefPtr<FinishCollectRunnable> mFinishCollectRunnable; + const bool mAnonymize; + + public: + CollectReportsRunnable(WorkerPrivate* aWorkerPrivate, + nsIHandleReportCallback* aHandleReport, + nsISupports* aHandlerData, bool aAnonymize, + const nsACString& aPath); + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + + ~CollectReportsRunnable() { + if (NS_IsMainThread()) { + mFinishCollectRunnable->Run(); + return; + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + MOZ_ALWAYS_SUCCEEDS(workerPrivate->DispatchToMainThreadForMessaging( + mFinishCollectRunnable.forget())); + } + }; + + class FinishCollectRunnable final : public Runnable { + nsCOMPtr<nsIHandleReportCallback> mHandleReport; + nsCOMPtr<nsISupports> mHandlerData; + size_t mPerformanceUserEntries; + size_t mPerformanceResourceEntries; + const bool mAnonymize; + bool mSuccess; + + public: + WorkerJSContextStats mCxStats; + + explicit FinishCollectRunnable(nsIHandleReportCallback* aHandleReport, + nsISupports* aHandlerData, bool aAnonymize, + const nsACString& aPath); + + NS_IMETHOD Run() override; + + void SetPerformanceSizes(size_t userEntries, size_t resourceEntries) { + mPerformanceUserEntries = userEntries; + mPerformanceResourceEntries = resourceEntries; + } + + void SetSuccess(bool success) { mSuccess = success; } + + FinishCollectRunnable(const FinishCollectRunnable&) = delete; + FinishCollectRunnable& operator=(const FinishCollectRunnable&) = delete; + FinishCollectRunnable& operator=(const FinishCollectRunnable&&) = delete; + + private: + ~FinishCollectRunnable() { + // mHandleReport and mHandlerData are released on the main thread. + AssertIsOnMainThread(); + } + }; + + ~MemoryReporter() = default; + + void Disable() { + // Called from WorkerPrivate::DisableMemoryReporter. + mMutex.AssertCurrentThreadOwns(); + + NS_ASSERTION(mWorkerPrivate, "Disabled more than once!"); + mWorkerPrivate = nullptr; + } +}; + +NS_IMPL_ISUPPORTS(WorkerPrivate::MemoryReporter, nsIMemoryReporter) + +NS_IMETHODIMP +WorkerPrivate::MemoryReporter::CollectReports( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize) { + AssertIsOnMainThread(); + + RefPtr<CollectReportsRunnable> runnable; + + { + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + // This will effectively report 0 memory. + nsCOMPtr<nsIMemoryReporterManager> manager = + do_GetService("@mozilla.org/memory-reporter-manager;1"); + if (manager) { + manager->EndReport(); + } + return NS_OK; + } + + nsAutoCString path; + path.AppendLiteral("explicit/workers/workers("); + if (aAnonymize && !mWorkerPrivate->Domain().IsEmpty()) { + path.AppendLiteral("<anonymized-domain>)/worker(<anonymized-url>"); + } else { + nsAutoCString escapedDomain(mWorkerPrivate->Domain()); + if (escapedDomain.IsEmpty()) { + escapedDomain += "chrome"; + } else { + escapedDomain.ReplaceChar('/', '\\'); + } + path.Append(escapedDomain); + path.AppendLiteral(")/worker("); + NS_ConvertUTF16toUTF8 escapedURL(mWorkerPrivate->ScriptURL()); + escapedURL.ReplaceChar('/', '\\'); + path.Append(escapedURL); + } + path.AppendPrintf(", 0x%p)/", static_cast<void*>(mWorkerPrivate)); + + runnable = new CollectReportsRunnable(mWorkerPrivate, aHandleReport, aData, + aAnonymize, path); + } + + if (!runnable->Dispatch()) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +WorkerPrivate::MemoryReporter::CollectReportsRunnable::CollectReportsRunnable( + WorkerPrivate* aWorkerPrivate, nsIHandleReportCallback* aHandleReport, + nsISupports* aHandlerData, bool aAnonymize, const nsACString& aPath) + : MainThreadWorkerControlRunnable(aWorkerPrivate), + mFinishCollectRunnable(new FinishCollectRunnable( + aHandleReport, aHandlerData, aAnonymize, aPath)), + mAnonymize(aAnonymize) {} + +bool WorkerPrivate::MemoryReporter::CollectReportsRunnable::WorkerRun( + JSContext* aCx, WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<WorkerGlobalScope> scope = aWorkerPrivate->GlobalScope(); + RefPtr<Performance> performance = + scope ? scope->GetPerformanceIfExists() : nullptr; + if (performance) { + size_t userEntries = performance->SizeOfUserEntries(JsWorkerMallocSizeOf); + size_t resourceEntries = + performance->SizeOfResourceEntries(JsWorkerMallocSizeOf); + mFinishCollectRunnable->SetPerformanceSizes(userEntries, resourceEntries); + } + + mFinishCollectRunnable->SetSuccess(aWorkerPrivate->CollectRuntimeStats( + &mFinishCollectRunnable->mCxStats, mAnonymize)); + + return true; +} + +WorkerPrivate::MemoryReporter::FinishCollectRunnable::FinishCollectRunnable( + nsIHandleReportCallback* aHandleReport, nsISupports* aHandlerData, + bool aAnonymize, const nsACString& aPath) + : mozilla::Runnable( + "dom::WorkerPrivate::MemoryReporter::FinishCollectRunnable"), + mHandleReport(aHandleReport), + mHandlerData(aHandlerData), + mPerformanceUserEntries(0), + mPerformanceResourceEntries(0), + mAnonymize(aAnonymize), + mSuccess(false), + mCxStats(aPath) {} + +NS_IMETHODIMP +WorkerPrivate::MemoryReporter::FinishCollectRunnable::Run() { + AssertIsOnMainThread(); + + nsCOMPtr<nsIMemoryReporterManager> manager = + do_GetService("@mozilla.org/memory-reporter-manager;1"); + + if (!manager) return NS_OK; + + if (mSuccess) { + xpc::ReportJSRuntimeExplicitTreeStats( + mCxStats, mCxStats.Path(), mHandleReport, mHandlerData, mAnonymize); + + if (mPerformanceUserEntries) { + nsCString path = mCxStats.Path(); + path.AppendLiteral("dom/performance/user-entries"); + mHandleReport->Callback(""_ns, path, nsIMemoryReporter::KIND_HEAP, + nsIMemoryReporter::UNITS_BYTES, + static_cast<int64_t>(mPerformanceUserEntries), + "Memory used for performance user entries."_ns, + mHandlerData); + } + + if (mPerformanceResourceEntries) { + nsCString path = mCxStats.Path(); + path.AppendLiteral("dom/performance/resource-entries"); + mHandleReport->Callback( + ""_ns, path, nsIMemoryReporter::KIND_HEAP, + nsIMemoryReporter::UNITS_BYTES, + static_cast<int64_t>(mPerformanceResourceEntries), + "Memory used for performance resource entries."_ns, mHandlerData); + } + } + + manager->EndReport(); + + return NS_OK; +} + +WorkerPrivate::SyncLoopInfo::SyncLoopInfo(EventTarget* aEventTarget) + : mEventTarget(aEventTarget), + mResult(NS_ERROR_FAILURE), + mCompleted(false) +#ifdef DEBUG + , + mHasRun(false) +#endif +{ +} + +Document* WorkerPrivate::GetDocument() const { + AssertIsOnMainThread(); + if (nsPIDOMWindowInner* window = GetAncestorWindow()) { + return window->GetExtantDoc(); + } + // couldn't query a document, give up and return nullptr + return nullptr; +} + +nsPIDOMWindowInner* WorkerPrivate::GetAncestorWindow() const { + AssertIsOnMainThread(); + if (mLoadInfo.mWindow) { + return mLoadInfo.mWindow; + } + // if we don't have a document, we should query the document + // from the parent in case of a nested worker + WorkerPrivate* parent = mParent; + while (parent) { + if (parent->mLoadInfo.mWindow) { + return parent->mLoadInfo.mWindow; + } + parent = parent->GetParent(); + } + // couldn't query a window, give up and return nullptr + return nullptr; +} + +class EvictFromBFCacheRunnable final : public WorkerProxyToMainThreadRunnable { + public: + void RunOnMainThread(WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + AssertIsOnMainThread(); + if (nsCOMPtr<nsPIDOMWindowInner> win = + aWorkerPrivate->GetAncestorWindow()) { + win->RemoveFromBFCacheSync(); + } + } + + void RunBackOnWorkerThreadForCleanup(WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } +}; + +void WorkerPrivate::EvictFromBFCache() { + AssertIsOnWorkerThread(); + RefPtr<EvictFromBFCacheRunnable> runnable = new EvictFromBFCacheRunnable(); + runnable->Dispatch(this); +} + +void WorkerPrivate::SetCsp(nsIContentSecurityPolicy* aCSP) { + AssertIsOnMainThread(); + if (!aCSP) { + return; + } + aCSP->EnsureEventTarget(mMainThreadEventTarget); + + mLoadInfo.mCSP = aCSP; + mLoadInfo.mCSPInfo = MakeUnique<CSPInfo>(); + nsresult rv = CSPToCSPInfo(mLoadInfo.mCSP, mLoadInfo.mCSPInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +nsresult WorkerPrivate::SetCSPFromHeaderValues( + const nsACString& aCSPHeaderValue, + const nsACString& aCSPReportOnlyHeaderValue) { + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(!mLoadInfo.mCSP); + + NS_ConvertASCIItoUTF16 cspHeaderValue(aCSPHeaderValue); + NS_ConvertASCIItoUTF16 cspROHeaderValue(aCSPReportOnlyHeaderValue); + + nsresult rv; + nsCOMPtr<nsIContentSecurityPolicy> csp = new nsCSPContext(); + + // First, we try to query the URI from the Principal, but + // in case selfURI remains empty (e.g in case the Principal + // is a SystemPrincipal) then we fall back and use the + // base URI as selfURI for CSP. + nsCOMPtr<nsIURI> selfURI; + // Its not recommended to use the BasePrincipal to get the URI + // but in this case we need to make an exception + auto* basePrin = BasePrincipal::Cast(mLoadInfo.mPrincipal); + if (basePrin) { + basePrin->GetURI(getter_AddRefs(selfURI)); + } + if (!selfURI) { + selfURI = mLoadInfo.mBaseURI; + } + MOZ_ASSERT(selfURI, "need a self URI for CSP"); + + rv = csp->SetRequestContextWithPrincipal(mLoadInfo.mPrincipal, selfURI, + u""_ns, 0); + NS_ENSURE_SUCCESS(rv, rv); + + csp->EnsureEventTarget(mMainThreadEventTarget); + + // If there's a CSP header, apply it. + if (!cspHeaderValue.IsEmpty()) { + rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false); + NS_ENSURE_SUCCESS(rv, rv); + } + // If there's a report-only CSP header, apply it. + if (!cspROHeaderValue.IsEmpty()) { + rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true); + NS_ENSURE_SUCCESS(rv, rv); + } + + RefPtr<extensions::WebExtensionPolicy> addonPolicy; + + if (basePrin) { + addonPolicy = basePrin->AddonPolicy(); + } + + // For extension workers there aren't any csp header values, + // instead it will inherit the Extension CSP. + if (addonPolicy) { + csp->AppendPolicy(addonPolicy->BaseCSP(), false, false); + csp->AppendPolicy(addonPolicy->ExtensionPageCSP(), false, false); + } + + mLoadInfo.mCSP = csp; + + // Set evalAllowed, default value is set in GetAllowsEval + bool evalAllowed = false; + bool reportEvalViolations = false; + rv = csp->GetAllowsEval(&reportEvalViolations, &evalAllowed); + NS_ENSURE_SUCCESS(rv, rv); + + mLoadInfo.mEvalAllowed = evalAllowed; + mLoadInfo.mReportEvalCSPViolations = reportEvalViolations; + + // Set wasmEvalAllowed + bool wasmEvalAllowed = false; + bool reportWasmEvalViolations = false; + rv = csp->GetAllowsWasmEval(&reportWasmEvalViolations, &wasmEvalAllowed); + NS_ENSURE_SUCCESS(rv, rv); + + // As for nsScriptSecurityManager::ContentSecurityPolicyPermitsJSAction, + // for MV2 extensions we have to allow wasm by default and report violations + // for historical reasons. + // TODO bug 1770909: remove this exception. + if (!wasmEvalAllowed && addonPolicy && addonPolicy->ManifestVersion() == 2) { + wasmEvalAllowed = true; + reportWasmEvalViolations = true; + } + + mLoadInfo.mWasmEvalAllowed = wasmEvalAllowed; + mLoadInfo.mReportWasmEvalCSPViolations = reportWasmEvalViolations; + + mLoadInfo.mCSPInfo = MakeUnique<CSPInfo>(); + rv = CSPToCSPInfo(csp, mLoadInfo.mCSPInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +void WorkerPrivate::StoreCSPOnClient() { + auto data = mWorkerThreadAccessible.Access(); + MOZ_ASSERT(data->mScope); + if (mLoadInfo.mCSPInfo) { + data->mScope->MutableClientSourceRef().SetCspInfo(*mLoadInfo.mCSPInfo); + } +} + +void WorkerPrivate::UpdateReferrerInfoFromHeader( + const nsACString& aReferrerPolicyHeaderValue) { + NS_ConvertUTF8toUTF16 headerValue(aReferrerPolicyHeaderValue); + + if (headerValue.IsEmpty()) { + return; + } + + ReferrerPolicy policy = + ReferrerInfo::ReferrerPolicyFromHeaderString(headerValue); + if (policy == ReferrerPolicy::_empty) { + return; + } + + nsCOMPtr<nsIReferrerInfo> referrerInfo = + static_cast<ReferrerInfo*>(GetReferrerInfo())->CloneWithNewPolicy(policy); + SetReferrerInfo(referrerInfo); +} + +void WorkerPrivate::Traverse(nsCycleCollectionTraversalCallback& aCb) { + AssertIsOnParentThread(); + + // The WorkerPrivate::mParentEventTargetRef has a reference to the exposed + // Worker object, which is really held by the worker thread. We traverse this + // reference if and only if all main thread event queues are empty, no + // shutdown tasks, no StrongWorkerRefs, no child workers, no timeouts, no + // blocking background actors, and we have not released the main thread + // reference. We do not unlink it. This allows the CC to break cycles + // involving the Worker and begin shutting it down (which does happen in + // unlink) but ensures that the WorkerPrivate won't be deleted before we're + // done shutting down the thread. + if (IsEligibleForCC() && !mMainThreadObjectsForgotten) { + nsCycleCollectionTraversalCallback& cb = aCb; + WorkerPrivate* tmp = this; + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParentEventTargetRef); + } +} + +nsresult WorkerPrivate::Dispatch(already_AddRefed<WorkerRunnable> aRunnable, + nsIEventTarget* aSyncLoopTarget) { + // May be called on any thread! + MutexAutoLock lock(mMutex); + return DispatchLockHeld(std::move(aRunnable), aSyncLoopTarget, lock); +} + +nsresult WorkerPrivate::DispatchLockHeld( + already_AddRefed<WorkerRunnable> aRunnable, nsIEventTarget* aSyncLoopTarget, + const MutexAutoLock& aProofOfLock) { + // May be called on any thread! + RefPtr<WorkerRunnable> runnable(aRunnable); + LOGV(("WorkerPrivate::DispatchLockHeld [%p] runnable: %p", this, + runnable.get())); + + MOZ_ASSERT_IF(aSyncLoopTarget, mThread); + + if (mStatus == Dead || (!aSyncLoopTarget && ParentStatus() > Canceling)) { + LOGV(("WorkerPrivate::DispatchLockHeld [%p] runnable %p, parent status: %u", + this, runnable.get(), (uint8_t)(ParentStatus()))); + NS_WARNING( + "A runnable was posted to a worker that is already shutting " + "down!"); + return NS_ERROR_UNEXPECTED; + } + + if (runnable->IsDebuggeeRunnable() && !mDebuggerReady) { + MOZ_RELEASE_ASSERT(!aSyncLoopTarget); + mDelayedDebuggeeRunnables.AppendElement(runnable); + return NS_OK; + } + + if (!mThread) { + if (ParentStatus() == Pending || mStatus == Pending) { + LOGV( + ("WorkerPrivate::DispatchLockHeld [%p] runnable %p is queued in " + "mPreStartRunnables", + this, runnable.get())); + mPreStartRunnables.AppendElement(runnable); + return NS_OK; + } + + NS_WARNING( + "Using a worker event target after the thread has already" + "been released!"); + return NS_ERROR_UNEXPECTED; + } + + nsresult rv; + if (aSyncLoopTarget) { + LOGV( + ("WorkerPrivate::DispatchLockHeld [%p] runnable %p dispatch to a " + "SyncLoop(%p)", + this, runnable.get(), aSyncLoopTarget)); + rv = aSyncLoopTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + } else { + // WorkerDebuggeeRunnables don't need any special treatment here. True, + // they should not be delivered to a frozen worker. But frozen workers + // aren't drawing from the thread's main event queue anyway, only from + // mControlQueue. + LOGV( + ("WorkerPrivate::DispatchLockHeld [%p] runnable %p dispatch to the " + "main event queue", + this, runnable.get())); + rv = mThread->DispatchAnyThread(WorkerThreadFriendKey(), runnable.forget()); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mCondVar.Notify(); + return NS_OK; +} + +void WorkerPrivate::EnableDebugger() { + AssertIsOnParentThread(); + + if (NS_FAILED(RegisterWorkerDebugger(this))) { + NS_WARNING("Failed to register worker debugger!"); + return; + } +} + +void WorkerPrivate::DisableDebugger() { + AssertIsOnParentThread(); + + // RegisterDebuggerMainThreadRunnable might be dispatched but not executed. + // Wait for its execution before unregistraion. + if (!NS_IsMainThread()) { + WaitForIsDebuggerRegistered(true); + } + + if (NS_FAILED(UnregisterWorkerDebugger(this))) { + NS_WARNING("Failed to unregister worker debugger!"); + } +} + +nsresult WorkerPrivate::DispatchControlRunnable( + already_AddRefed<WorkerControlRunnable> aWorkerControlRunnable) { + // May be called on any thread! + RefPtr<WorkerControlRunnable> runnable(aWorkerControlRunnable); + MOZ_ASSERT(runnable); + + LOG(WorkerLog(), ("WorkerPrivate::DispatchControlRunnable [%p] runnable %p", + this, runnable.get())); + + { + MutexAutoLock lock(mMutex); + + if (mStatus == Dead) { + return NS_ERROR_UNEXPECTED; + } + + // Transfer ownership to the control queue. + mControlQueue.Push(runnable.forget().take()); + + if (JSContext* cx = mJSContext) { + MOZ_ASSERT(mThread); + JS_RequestInterruptCallback(cx); + } + + mCondVar.Notify(); + } + + return NS_OK; +} + +nsresult WorkerPrivate::DispatchDebuggerRunnable( + already_AddRefed<WorkerRunnable> aDebuggerRunnable) { + // May be called on any thread! + + RefPtr<WorkerRunnable> runnable(aDebuggerRunnable); + + MOZ_ASSERT(runnable); + + { + MutexAutoLock lock(mMutex); + + if (mStatus == Dead) { + NS_WARNING( + "A debugger runnable was posted to a worker that is already " + "shutting down!"); + return NS_ERROR_UNEXPECTED; + } + + // Transfer ownership to the debugger queue. + mDebuggerQueue.Push(runnable.forget().take()); + + mCondVar.Notify(); + } + + return NS_OK; +} + +already_AddRefed<WorkerRunnable> WorkerPrivate::MaybeWrapAsWorkerRunnable( + already_AddRefed<nsIRunnable> aRunnable) { + // May be called on any thread! + + nsCOMPtr<nsIRunnable> runnable(aRunnable); + MOZ_ASSERT(runnable); + + LOGV(("WorkerPrivate::MaybeWrapAsWorkerRunnable [%p] runnable: %p", this, + runnable.get())); + + RefPtr<WorkerRunnable> workerRunnable = + WorkerRunnable::FromRunnable(runnable); + if (workerRunnable) { + return workerRunnable.forget(); + } + + workerRunnable = new ExternalRunnableWrapper(this, runnable); + return workerRunnable.forget(); +} + +bool WorkerPrivate::Start() { + // May be called on any thread! + LOG(WorkerLog(), ("WorkerPrivate::Start [%p]", this)); + { + MutexAutoLock lock(mMutex); + NS_ASSERTION(mParentStatus != Running, "How can this be?!"); + + if (mParentStatus == Pending) { + mParentStatus = Running; + return true; + } + } + + return false; +} + +// aCx is null when called from the finalizer +bool WorkerPrivate::Notify(WorkerStatus aStatus) { + AssertIsOnParentThread(); + // This method is only called for Canceling or later. + MOZ_DIAGNOSTIC_ASSERT(aStatus >= Canceling); + + bool pending; + { + MutexAutoLock lock(mMutex); + + if (mParentStatus >= aStatus) { + return true; + } + + pending = mParentStatus == Pending; + mParentStatus = aStatus; + } + + if (mCancellationCallback) { + mCancellationCallback(!pending); + mCancellationCallback = nullptr; + } + + if (pending) { +#ifdef DEBUG + { + // Fake a thread here just so that our assertions don't go off for no + // reason. + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + MOZ_ASSERT(!mPRThread); + mPRThread = PRThreadFromThread(currentThread); + MOZ_ASSERT(mPRThread); + } +#endif + + // Worker never got a chance to run, go ahead and delete it. + ScheduleDeletion(WorkerPrivate::WorkerNeverRan); + return true; + } + + // No Canceling timeout is needed. + if (mCancelingTimer) { + mCancelingTimer->Cancel(); + mCancelingTimer = nullptr; + } + + // The NotifyRunnable kicks off a series of events that need the + // CancelingOnParentRunnable to be executed always. + // Note that we already advanced mParentStatus above and we check that + // status in all other (asynchronous) call sites of SetIsPaused. + if (!mParent) { + MOZ_ALWAYS_SUCCEEDS(mMainThreadDebuggeeEventTarget->SetIsPaused(false)); + } + + RefPtr<NotifyRunnable> runnable = new NotifyRunnable(this, aStatus); + return runnable->Dispatch(); +} + +bool WorkerPrivate::Freeze(const nsPIDOMWindowInner* aWindow) { + AssertIsOnParentThread(); + + mParentFrozen = true; + + bool isCanceling = false; + { + MutexAutoLock lock(mMutex); + + isCanceling = mParentStatus >= Canceling; + } + + // WorkerDebuggeeRunnables sent from a worker to content must not be + // delivered while the worker is frozen. + // + // Since a top-level worker and all its children share the same + // mMainThreadDebuggeeEventTarget, it's sufficient to do this only in the + // top-level worker. + if (aWindow) { + // This is called from WorkerPrivate construction, and We may not have + // allocated mMainThreadDebuggeeEventTarget yet. + if (mMainThreadDebuggeeEventTarget) { + // Pausing a ThrottledEventQueue is infallible. + MOZ_ALWAYS_SUCCEEDS( + mMainThreadDebuggeeEventTarget->SetIsPaused(!isCanceling)); + } + } + + if (isCanceling) { + return true; + } + + DisableDebugger(); + + RefPtr<FreezeRunnable> runnable = new FreezeRunnable(this); + return runnable->Dispatch(); +} + +bool WorkerPrivate::Thaw(const nsPIDOMWindowInner* aWindow) { + AssertIsOnParentThread(); + MOZ_ASSERT(mParentFrozen); + + mParentFrozen = false; + + { + bool isCanceling = false; + + { + MutexAutoLock lock(mMutex); + + isCanceling = mParentStatus >= Canceling; + } + + // Delivery of WorkerDebuggeeRunnables to the window may resume. + // + // Since a top-level worker and all its children share the same + // mMainThreadDebuggeeEventTarget, it's sufficient to do this only in the + // top-level worker. + if (aWindow) { + // Since the worker is no longer frozen, only a paused parent window + // should require the queue to remain paused. + // + // This can only fail if the ThrottledEventQueue cannot dispatch its + // executor to the main thread, in which case the main thread was never + // going to draw runnables from it anyway, so the failure doesn't matter. + Unused << mMainThreadDebuggeeEventTarget->SetIsPaused( + IsParentWindowPaused() && !isCanceling); + } + + if (isCanceling) { + return true; + } + } + + EnableDebugger(); + + RefPtr<ThawRunnable> runnable = new ThawRunnable(this); + return runnable->Dispatch(); +} + +void WorkerPrivate::ParentWindowPaused() { + AssertIsOnMainThread(); + MOZ_ASSERT(!mParentWindowPaused); + mParentWindowPaused = true; + + // This is called from WorkerPrivate construction, and we may not have + // allocated mMainThreadDebuggeeEventTarget yet. + if (mMainThreadDebuggeeEventTarget) { + bool isCanceling = false; + + { + MutexAutoLock lock(mMutex); + + isCanceling = mParentStatus >= Canceling; + } + + // If we are already canceling we might wait for CancelingOnParentRunnable + // to be executed, so do not pause. + MOZ_ALWAYS_SUCCEEDS( + mMainThreadDebuggeeEventTarget->SetIsPaused(!isCanceling)); + } +} + +void WorkerPrivate::ParentWindowResumed() { + AssertIsOnMainThread(); + + MOZ_ASSERT(mParentWindowPaused); + mParentWindowPaused = false; + + bool isCanceling = false; + { + MutexAutoLock lock(mMutex); + + isCanceling = mParentStatus >= Canceling; + } + + // Since the window is no longer paused, the queue should only remain paused + // if the worker is frozen. + // + // This can only fail if the ThrottledEventQueue cannot dispatch its executor + // to the main thread, in which case the main thread was never going to draw + // runnables from it anyway, so the failure doesn't matter. + Unused << mMainThreadDebuggeeEventTarget->SetIsPaused(IsFrozen() && + !isCanceling); +} + +void WorkerPrivate::PropagateStorageAccessPermissionGranted() { + AssertIsOnParentThread(); + + { + MutexAutoLock lock(mMutex); + + if (mParentStatus >= Canceling) { + return; + } + } + + RefPtr<PropagateStorageAccessPermissionGrantedRunnable> runnable = + new PropagateStorageAccessPermissionGrantedRunnable(this); + Unused << NS_WARN_IF(!runnable->Dispatch()); +} + +bool WorkerPrivate::Close() { + mMutex.AssertCurrentThreadOwns(); + if (mParentStatus < Closing) { + mParentStatus = Closing; + } + + return true; +} + +bool WorkerPrivate::ProxyReleaseMainThreadObjects() { + AssertIsOnParentThread(); + MOZ_ASSERT(!mMainThreadObjectsForgotten); + + nsCOMPtr<nsILoadGroup> loadGroupToCancel; + // If we're not overriden, then do nothing here. Let the load group get + // handled in ForgetMainThreadObjects(). + if (mLoadInfo.mInterfaceRequestor) { + mLoadInfo.mLoadGroup.swap(loadGroupToCancel); + } + + bool result = mLoadInfo.ProxyReleaseMainThreadObjects( + this, std::move(loadGroupToCancel)); + + mMainThreadObjectsForgotten = true; + + return result; +} + +void WorkerPrivate::UpdateContextOptions( + const JS::ContextOptions& aContextOptions) { + AssertIsOnParentThread(); + + { + MutexAutoLock lock(mMutex); + mJSSettings.contextOptions = aContextOptions; + } + + RefPtr<UpdateContextOptionsRunnable> runnable = + new UpdateContextOptionsRunnable(this, aContextOptions); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update worker context options!"); + } +} + +void WorkerPrivate::UpdateLanguages(const nsTArray<nsString>& aLanguages) { + AssertIsOnParentThread(); + + RefPtr<UpdateLanguagesRunnable> runnable = + new UpdateLanguagesRunnable(this, aLanguages); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update worker languages!"); + } +} + +void WorkerPrivate::UpdateJSWorkerMemoryParameter(JSGCParamKey aKey, + Maybe<uint32_t> aValue) { + AssertIsOnParentThread(); + + bool changed = false; + + { + MutexAutoLock lock(mMutex); + changed = mJSSettings.ApplyGCSetting(aKey, aValue); + } + + if (changed) { + RefPtr<UpdateJSWorkerMemoryParameterRunnable> runnable = + new UpdateJSWorkerMemoryParameterRunnable(this, aKey, aValue); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update memory parameter!"); + } + } +} + +#ifdef JS_GC_ZEAL +void WorkerPrivate::UpdateGCZeal(uint8_t aGCZeal, uint32_t aFrequency) { + AssertIsOnParentThread(); + + { + MutexAutoLock lock(mMutex); + mJSSettings.gcZeal = aGCZeal; + mJSSettings.gcZealFrequency = aFrequency; + } + + RefPtr<UpdateGCZealRunnable> runnable = + new UpdateGCZealRunnable(this, aGCZeal, aFrequency); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update worker gczeal!"); + } +} +#endif + +void WorkerPrivate::SetLowMemoryState(bool aState) { + AssertIsOnParentThread(); + + RefPtr<SetLowMemoryStateRunnable> runnable = + new SetLowMemoryStateRunnable(this, aState); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to set low memory state!"); + } +} + +void WorkerPrivate::GarbageCollect(bool aShrinking) { + AssertIsOnParentThread(); + + RefPtr<GarbageCollectRunnable> runnable = new GarbageCollectRunnable( + this, aShrinking, /* aCollectChildren = */ true); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to GC worker!"); + } +} + +void WorkerPrivate::CycleCollect() { + AssertIsOnParentThread(); + + RefPtr<CycleCollectRunnable> runnable = + new CycleCollectRunnable(this, /* aCollectChildren = */ true); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to CC worker!"); + } +} + +void WorkerPrivate::OfflineStatusChangeEvent(bool aIsOffline) { + AssertIsOnParentThread(); + + RefPtr<OfflineStatusChangeRunnable> runnable = + new OfflineStatusChangeRunnable(this, aIsOffline); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to dispatch offline status change event!"); + } +} + +void WorkerPrivate::OfflineStatusChangeEventInternal(bool aIsOffline) { + auto data = mWorkerThreadAccessible.Access(); + + // The worker is already in this state. No need to dispatch an event. + if (data->mOnLine == !aIsOffline) { + return; + } + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); ++index) { + data->mChildWorkers[index]->OfflineStatusChangeEvent(aIsOffline); + } + + data->mOnLine = !aIsOffline; + WorkerGlobalScope* globalScope = GlobalScope(); + RefPtr<WorkerNavigator> nav = globalScope->GetExistingNavigator(); + if (nav) { + nav->SetOnLine(data->mOnLine); + } + + nsString eventType; + if (aIsOffline) { + eventType.AssignLiteral("offline"); + } else { + eventType.AssignLiteral("online"); + } + + RefPtr<Event> event = NS_NewDOMEvent(globalScope, nullptr, nullptr); + + event->InitEvent(eventType, false, false); + event->SetTrusted(true); + + globalScope->DispatchEvent(*event); +} + +void WorkerPrivate::MemoryPressure() { + AssertIsOnParentThread(); + + RefPtr<MemoryPressureRunnable> runnable = new MemoryPressureRunnable(this); + Unused << NS_WARN_IF(!runnable->Dispatch()); +} + +RefPtr<WorkerPrivate::JSMemoryUsagePromise> WorkerPrivate::GetJSMemoryUsage() { + AssertIsOnMainThread(); + + { + MutexAutoLock lock(mMutex); + // If we have started shutting down the worker, do not dispatch a runnable + // to measure its memory. + if (ParentStatus() > Running) { + return nullptr; + } + } + + return InvokeAsync(ControlEventTarget(), __func__, []() { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(wp); + wp->AssertIsOnWorkerThread(); + MutexAutoLock lock(wp->mMutex); + return JSMemoryUsagePromise::CreateAndResolve( + js::GetGCHeapUsage(wp->mJSContext), __func__); + }); +} + +void WorkerPrivate::WorkerScriptLoaded() { + AssertIsOnMainThread(); + + if (IsSharedWorker() || IsServiceWorker()) { + // No longer need to hold references to the window or document we came from. + mLoadInfo.mWindow = nullptr; + mLoadInfo.mScriptContext = nullptr; + } +} + +void WorkerPrivate::SetBaseURI(nsIURI* aBaseURI) { + AssertIsOnMainThread(); + + if (!mLoadInfo.mBaseURI) { + NS_ASSERTION(GetParent(), "Shouldn't happen without a parent!"); + mLoadInfo.mResolvedScriptURI = aBaseURI; + } + + mLoadInfo.mBaseURI = aBaseURI; + + if (NS_FAILED(aBaseURI->GetSpec(mLocationInfo.mHref))) { + mLocationInfo.mHref.Truncate(); + } + + mLocationInfo.mHostname.Truncate(); + nsContentUtils::GetHostOrIPv6WithBrackets(aBaseURI, mLocationInfo.mHostname); + + nsCOMPtr<nsIURL> url(do_QueryInterface(aBaseURI)); + if (!url || NS_FAILED(url->GetFilePath(mLocationInfo.mPathname))) { + mLocationInfo.mPathname.Truncate(); + } + + nsCString temp; + + if (url && NS_SUCCEEDED(url->GetQuery(temp)) && !temp.IsEmpty()) { + mLocationInfo.mSearch.Assign('?'); + mLocationInfo.mSearch.Append(temp); + } + + if (NS_SUCCEEDED(aBaseURI->GetRef(temp)) && !temp.IsEmpty()) { + if (mLocationInfo.mHash.IsEmpty()) { + mLocationInfo.mHash.Assign('#'); + mLocationInfo.mHash.Append(temp); + } + } + + if (NS_SUCCEEDED(aBaseURI->GetScheme(mLocationInfo.mProtocol))) { + mLocationInfo.mProtocol.Append(':'); + } else { + mLocationInfo.mProtocol.Truncate(); + } + + int32_t port; + if (NS_SUCCEEDED(aBaseURI->GetPort(&port)) && port != -1) { + mLocationInfo.mPort.AppendInt(port); + + nsAutoCString host(mLocationInfo.mHostname); + host.Append(':'); + host.Append(mLocationInfo.mPort); + + mLocationInfo.mHost.Assign(host); + } else { + mLocationInfo.mHost.Assign(mLocationInfo.mHostname); + } + + nsContentUtils::GetWebExposedOriginSerialization(aBaseURI, + mLocationInfo.mOrigin); +} + +nsresult WorkerPrivate::SetPrincipalsAndCSPOnMainThread( + nsIPrincipal* aPrincipal, nsIPrincipal* aPartitionedPrincipal, + nsILoadGroup* aLoadGroup, nsIContentSecurityPolicy* aCsp) { + return mLoadInfo.SetPrincipalsAndCSPOnMainThread( + aPrincipal, aPartitionedPrincipal, aLoadGroup, aCsp); +} + +nsresult WorkerPrivate::SetPrincipalsAndCSPFromChannel(nsIChannel* aChannel) { + return mLoadInfo.SetPrincipalsAndCSPFromChannel(aChannel); +} + +bool WorkerPrivate::FinalChannelPrincipalIsValid(nsIChannel* aChannel) { + return mLoadInfo.FinalChannelPrincipalIsValid(aChannel); +} + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED +bool WorkerPrivate::PrincipalURIMatchesScriptURL() { + return mLoadInfo.PrincipalURIMatchesScriptURL(); +} +#endif + +void WorkerPrivate::UpdateOverridenLoadGroup(nsILoadGroup* aBaseLoadGroup) { + AssertIsOnMainThread(); + + // The load group should have been overriden at init time. + mLoadInfo.mInterfaceRequestor->MaybeAddBrowserChild(aBaseLoadGroup); +} + +bool WorkerPrivate::IsOnParentThread() const { + if (GetParent()) { + return GetParent()->IsOnWorkerThread(); + } + return NS_IsMainThread(); +} + +#ifdef DEBUG + +void WorkerPrivate::AssertIsOnParentThread() const { + if (GetParent()) { + GetParent()->AssertIsOnWorkerThread(); + } else { + AssertIsOnMainThread(); + } +} + +void WorkerPrivate::AssertInnerWindowIsCorrect() const { + AssertIsOnParentThread(); + + // Only care about top level workers from windows. + if (mParent || !mLoadInfo.mWindow) { + return; + } + + AssertIsOnMainThread(); + + nsPIDOMWindowOuter* outer = mLoadInfo.mWindow->GetOuterWindow(); + NS_ASSERTION(outer && outer->GetCurrentInnerWindow() == mLoadInfo.mWindow, + "Inner window no longer correct!"); +} + +#endif + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED +bool WorkerPrivate::PrincipalIsValid() const { + return mLoadInfo.PrincipalIsValid(); +} +#endif + +WorkerPrivate::WorkerThreadAccessible::WorkerThreadAccessible( + WorkerPrivate* const aParent) + : mNumWorkerRefsPreventingShutdownStart(0), + mDebuggerEventLoopLevel(0), + mNonblockingCCBackgroundActorCount(0), + mErrorHandlerRecursionCount(0), + mNextTimeoutId(1), + mCurrentTimerNestingLevel(0), + mFrozen(false), + mTimerRunning(false), + mRunningExpiredTimeouts(false), + mPeriodicGCTimerRunning(false), + mIdleGCTimerRunning(false), + mOnLine(aParent ? aParent->OnLine() : !NS_IsOffline()), + mJSThreadExecutionGranted(false), + mCCCollectedAnything(false) {} + +namespace { + +bool IsNewWorkerSecureContext(const WorkerPrivate* const aParent, + const WorkerKind aWorkerKind, + const WorkerLoadInfo& aLoadInfo) { + if (aParent) { + return aParent->IsSecureContext(); + } + + // Our secure context state depends on the kind of worker we have. + + if (aLoadInfo.mPrincipal && aLoadInfo.mPrincipal->IsSystemPrincipal()) { + return true; + } + + if (aWorkerKind == WorkerKindService) { + return true; + } + + if (aLoadInfo.mSecureContext != WorkerLoadInfo::eNotSet) { + return aLoadInfo.mSecureContext == WorkerLoadInfo::eSecureContext; + } + + MOZ_ASSERT_UNREACHABLE( + "non-chrome worker that is not a service worker " + "that has no parent and no associated window"); + + return false; +} + +} // namespace + +WorkerPrivate::WorkerPrivate( + WorkerPrivate* aParent, const nsAString& aScriptURL, bool aIsChromeWorker, + WorkerKind aWorkerKind, RequestCredentials aRequestCredentials, + enum WorkerType aWorkerType, const nsAString& aWorkerName, + const nsACString& aServiceWorkerScope, WorkerLoadInfo& aLoadInfo, + nsString&& aId, const nsID& aAgentClusterId, + const nsILoadInfo::CrossOriginOpenerPolicy aAgentClusterOpenerPolicy, + CancellationCallback&& aCancellationCallback, + TerminationCallback&& aTerminationCallback) + : mMutex("WorkerPrivate Mutex"), + mCondVar(mMutex, "WorkerPrivate CondVar"), + mParent(aParent), + mScriptURL(aScriptURL), + mWorkerName(aWorkerName), + mCredentialsMode(aRequestCredentials), + mWorkerType(aWorkerType), // If the worker runs as a script or a module + mWorkerKind(aWorkerKind), + mCancellationCallback(std::move(aCancellationCallback)), + mTerminationCallback(std::move(aTerminationCallback)), + mLoadInfo(std::move(aLoadInfo)), + mDebugger(nullptr), + mJSContext(nullptr), + mPRThread(nullptr), + mWorkerControlEventTarget(new WorkerEventTarget( + this, WorkerEventTarget::Behavior::ControlOnly)), + mWorkerHybridEventTarget( + new WorkerEventTarget(this, WorkerEventTarget::Behavior::Hybrid)), + mParentStatus(Pending), + mStatus(Pending), + mCreationTimeStamp(TimeStamp::Now()), + mCreationTimeHighRes((double)PR_Now() / PR_USEC_PER_MSEC), + mReportedUseCounters(false), + mAgentClusterId(aAgentClusterId), + mWorkerThreadAccessible(aParent), + mPostSyncLoopOperations(0), + mParentWindowPaused(false), + mWorkerScriptExecutedSuccessfully(false), + mFetchHandlerWasAdded(false), + mMainThreadObjectsForgotten(false), + mIsChromeWorker(aIsChromeWorker), + mParentFrozen(false), + mIsSecureContext( + IsNewWorkerSecureContext(mParent, mWorkerKind, mLoadInfo)), + mDebuggerRegistered(false), + mDebuggerReady(true), + mExtensionAPIAllowed(false), + mIsInAutomation(false), + mId(std::move(aId)), + mAgentClusterOpenerPolicy(aAgentClusterOpenerPolicy), + mIsPrivilegedAddonGlobal(false), + mTopLevelWorkerFinishedRunnableCount(0), + mWorkerFinishedRunnableCount(0) { + LOG(WorkerLog(), ("WorkerPrivate::WorkerPrivate [%p]", this)); + MOZ_ASSERT_IF(!IsDedicatedWorker(), NS_IsMainThread()); + + if (aParent) { + aParent->AssertIsOnWorkerThread(); + + // Note that this copies our parent's secure context state into mJSSettings. + aParent->CopyJSSettings(mJSSettings); + + MOZ_ASSERT_IF(mIsChromeWorker, mIsSecureContext); + + mIsInAutomation = aParent->IsInAutomation(); + + MOZ_ASSERT(IsDedicatedWorker()); + + if (aParent->mParentFrozen) { + Freeze(nullptr); + } + + mIsPrivilegedAddonGlobal = aParent->mIsPrivilegedAddonGlobal; + } else { + AssertIsOnMainThread(); + + RuntimeService::GetDefaultJSSettings(mJSSettings); + + { + JS::RealmOptions& chromeRealmOptions = mJSSettings.chromeRealmOptions; + JS::RealmOptions& contentRealmOptions = mJSSettings.contentRealmOptions; + + xpc::InitGlobalObjectOptions( + chromeRealmOptions, UsesSystemPrincipal(), mIsSecureContext, + ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC), + ShouldResistFingerprinting(RFPTarget::JSMathFdlibm), + ShouldResistFingerprinting(RFPTarget::JSLocale)); + xpc::InitGlobalObjectOptions( + contentRealmOptions, UsesSystemPrincipal(), mIsSecureContext, + ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC), + ShouldResistFingerprinting(RFPTarget::JSMathFdlibm), + ShouldResistFingerprinting(RFPTarget::JSLocale)); + + // Check if it's a privileged addon executing in order to allow access + // to SharedArrayBuffer + if (mLoadInfo.mPrincipal) { + if (auto* policy = + BasePrincipal::Cast(mLoadInfo.mPrincipal)->AddonPolicy()) { + if (policy->IsPrivileged() && + ExtensionPolicyService::GetSingleton().IsExtensionProcess()) { + // Privileged extensions are allowed to use SharedArrayBuffer in + // their extension process, but never in content scripts in + // content processes. + mIsPrivilegedAddonGlobal = true; + } + + if (StaticPrefs:: + extensions_backgroundServiceWorker_enabled_AtStartup() && + mWorkerKind == WorkerKindService && + policy->IsManifestBackgroundWorker(mScriptURL)) { + // Only allows ExtensionAPI for extension service workers + // that are declared in the extension manifest json as + // the background service worker. + mExtensionAPIAllowed = true; + } + } + } + + // The SharedArrayBuffer global constructor property should not be present + // in a fresh global object when shared memory objects aren't allowed + // (because COOP/COEP support isn't enabled, or because COOP/COEP don't + // act to isolate this worker to a separate process). + const bool defineSharedArrayBufferConstructor = IsSharedMemoryAllowed(); + chromeRealmOptions.creationOptions() + .setDefineSharedArrayBufferConstructor( + defineSharedArrayBufferConstructor); + contentRealmOptions.creationOptions() + .setDefineSharedArrayBufferConstructor( + defineSharedArrayBufferConstructor); + } + + mIsInAutomation = xpc::IsInAutomation(); + + // Our parent can get suspended after it initiates the async creation + // of a new worker thread. In this case suspend the new worker as well. + if (mLoadInfo.mWindow && mLoadInfo.mWindow->IsSuspended()) { + ParentWindowPaused(); + } + + if (mLoadInfo.mWindow && mLoadInfo.mWindow->IsFrozen()) { + Freeze(mLoadInfo.mWindow); + } + } + + nsCOMPtr<nsISerialEventTarget> target; + + // A child worker just inherits the parent workers ThrottledEventQueue + // and main thread target for now. This is mainly due to the restriction + // that ThrottledEventQueue can only be created on the main thread at the + // moment. + if (aParent) { + mMainThreadEventTargetForMessaging = + aParent->mMainThreadEventTargetForMessaging; + mMainThreadEventTarget = aParent->mMainThreadEventTarget; + mMainThreadDebuggeeEventTarget = aParent->mMainThreadDebuggeeEventTarget; + return; + } + + MOZ_ASSERT(NS_IsMainThread()); + target = GetWindow() + ? GetWindow()->GetBrowsingContextGroup()->GetWorkerEventQueue() + : nullptr; + + if (!target) { + target = GetMainThreadSerialEventTarget(); + MOZ_DIAGNOSTIC_ASSERT(target); + } + + // Throttle events to the main thread using a ThrottledEventQueue specific to + // this tree of worker threads. + mMainThreadEventTargetForMessaging = + ThrottledEventQueue::Create(target, "Worker queue for messaging"); + if (StaticPrefs::dom_worker_use_medium_high_event_queue()) { + mMainThreadEventTarget = ThrottledEventQueue::Create( + GetMainThreadSerialEventTarget(), "Worker queue", + nsIRunnablePriority::PRIORITY_MEDIUMHIGH); + } else { + mMainThreadEventTarget = mMainThreadEventTargetForMessaging; + } + mMainThreadDebuggeeEventTarget = + ThrottledEventQueue::Create(target, "Worker debuggee queue"); + if (IsParentWindowPaused() || IsFrozen()) { + MOZ_ALWAYS_SUCCEEDS(mMainThreadDebuggeeEventTarget->SetIsPaused(true)); + } +} + +WorkerPrivate::~WorkerPrivate() { + MOZ_DIAGNOSTIC_ASSERT(mTopLevelWorkerFinishedRunnableCount == 0); + MOZ_DIAGNOSTIC_ASSERT(mWorkerFinishedRunnableCount == 0); + + mWorkerControlEventTarget->ForgetWorkerPrivate(this); + + // We force the hybrid event target to forget the thread when we + // enter the Killing state, but we do it again here to be safe. + // Its possible that we may be created and destroyed without progressing + // to Killing via some obscure code path. + mWorkerHybridEventTarget->ForgetWorkerPrivate(this); +} + +WorkerPrivate::AgentClusterIdAndCoop +WorkerPrivate::ComputeAgentClusterIdAndCoop(WorkerPrivate* aParent, + WorkerKind aWorkerKind, + WorkerLoadInfo* aLoadInfo) { + nsILoadInfo::CrossOriginOpenerPolicy agentClusterCoop = + nsILoadInfo::OPENER_POLICY_UNSAFE_NONE; + + if (aParent) { + MOZ_ASSERT(aWorkerKind == WorkerKind::WorkerKindDedicated); + + return {aParent->AgentClusterId(), aParent->mAgentClusterOpenerPolicy}; + } + + AssertIsOnMainThread(); + + if (aWorkerKind == WorkerKind::WorkerKindService || + aWorkerKind == WorkerKind::WorkerKindShared) { + return {aLoadInfo->mAgentClusterId, agentClusterCoop}; + } + + if (aLoadInfo->mWindow) { + Document* doc = aLoadInfo->mWindow->GetExtantDoc(); + MOZ_DIAGNOSTIC_ASSERT(doc); + RefPtr<DocGroup> docGroup = doc->GetDocGroup(); + + nsID agentClusterId = + docGroup ? docGroup->AgentClusterId() : nsID::GenerateUUID(); + + BrowsingContext* bc = aLoadInfo->mWindow->GetBrowsingContext(); + MOZ_DIAGNOSTIC_ASSERT(bc); + return {agentClusterId, bc->Top()->GetOpenerPolicy()}; + } + + // If the window object was failed to be set into the WorkerLoadInfo, we + // make the worker into another agent cluster group instead of failures. + return {nsID::GenerateUUID(), agentClusterCoop}; +} + +// static +already_AddRefed<WorkerPrivate> WorkerPrivate::Constructor( + JSContext* aCx, const nsAString& aScriptURL, bool aIsChromeWorker, + WorkerKind aWorkerKind, RequestCredentials aRequestCredentials, + enum WorkerType aWorkerType, const nsAString& aWorkerName, + const nsACString& aServiceWorkerScope, WorkerLoadInfo* aLoadInfo, + ErrorResult& aRv, nsString aId, + CancellationCallback&& aCancellationCallback, + TerminationCallback&& aTerminationCallback) { + WorkerPrivate* parent = + NS_IsMainThread() ? nullptr : GetCurrentThreadWorkerPrivate(); + + // If this is a sub-worker, we need to keep the parent worker alive until this + // one is registered. + RefPtr<StrongWorkerRef> workerRef; + if (parent) { + parent->AssertIsOnWorkerThread(); + + workerRef = StrongWorkerRef::Create(parent, "WorkerPrivate::Constructor"); + if (NS_WARN_IF(!workerRef)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + } else { + AssertIsOnMainThread(); + } + + Maybe<WorkerLoadInfo> stackLoadInfo; + if (!aLoadInfo) { + stackLoadInfo.emplace(); + + nsresult rv = GetLoadInfo( + aCx, nullptr, parent, aScriptURL, aWorkerType, aRequestCredentials, + aIsChromeWorker, InheritLoadGroup, aWorkerKind, stackLoadInfo.ptr()); + aRv.MightThrowJSException(); + if (NS_FAILED(rv)) { + workerinternals::ReportLoadError(aRv, rv, aScriptURL); + return nullptr; + } + + aLoadInfo = stackLoadInfo.ptr(); + } + + // NB: This has to be done before creating the WorkerPrivate, because it will + // attempt to use static variables that are initialized in the RuntimeService + // constructor. + RuntimeService* runtimeService; + + if (!parent) { + runtimeService = RuntimeService::GetOrCreateService(); + if (!runtimeService) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + } else { + runtimeService = RuntimeService::GetService(); + } + + MOZ_ASSERT(runtimeService); + + // Don't create a worker with the shutting down RuntimeService. + if (runtimeService->IsShuttingDown()) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + AgentClusterIdAndCoop idAndCoop = + ComputeAgentClusterIdAndCoop(parent, aWorkerKind, aLoadInfo); + + RefPtr<WorkerPrivate> worker = new WorkerPrivate( + parent, aScriptURL, aIsChromeWorker, aWorkerKind, aRequestCredentials, + aWorkerType, aWorkerName, aServiceWorkerScope, *aLoadInfo, std::move(aId), + idAndCoop.mId, idAndCoop.mCoop, std::move(aCancellationCallback), + std::move(aTerminationCallback)); + + // Gecko contexts always have an explicitly-set default locale (set by + // XPJSRuntime::Initialize for the main thread, set by + // WorkerThreadPrimaryRunnable::Run for workers just before running worker + // code), so this is never SpiderMonkey's builtin default locale. + JS::UniqueChars defaultLocale = JS_GetDefaultLocale(aCx); + if (NS_WARN_IF(!defaultLocale)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + worker->mDefaultLocale = std::move(defaultLocale); + + if (!runtimeService->RegisterWorker(*worker)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + // From this point on (worker thread has been started) we + // must keep ourself alive. We can now only be cleared by + // ClearSelfAndParentEventTargetRef(). + worker->mSelfRef = worker; + + worker->EnableDebugger(); + + MOZ_DIAGNOSTIC_ASSERT(worker->PrincipalIsValid()); + + UniquePtr<SerializedStackHolder> stack; + if (worker->IsWatchedByDevTools()) { + stack = GetCurrentStackForNetMonitor(aCx); + } + + // This should be non-null for dedicated workers and null for Shared and + // Service workers. All Encoding values are static and will live as long + // as the process and the convention is to therefore use raw pointers. + const mozilla::Encoding* aDocumentEncoding = + NS_IsMainThread() && !worker->GetParent() && worker->GetDocument() + ? worker->GetDocument()->GetDocumentCharacterSet().get() + : nullptr; + + RefPtr<CompileScriptRunnable> compiler = new CompileScriptRunnable( + worker, std::move(stack), aScriptURL, aDocumentEncoding); + if (!compiler->Dispatch()) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return worker.forget(); +} + +nsresult WorkerPrivate::SetIsDebuggerReady(bool aReady) { + AssertIsOnMainThread(); + MutexAutoLock lock(mMutex); + + if (mDebuggerReady == aReady) { + return NS_OK; + } + + if (!aReady && mDebuggerRegistered) { + // The debugger can only be marked as not ready during registration. + return NS_ERROR_FAILURE; + } + + mDebuggerReady = aReady; + + if (aReady && mDebuggerRegistered) { + // Dispatch all the delayed runnables without releasing the lock, to ensure + // that the order in which debuggee runnables execute is the same as the + // order in which they were originally dispatched. + auto pending = std::move(mDelayedDebuggeeRunnables); + for (uint32_t i = 0; i < pending.Length(); i++) { + RefPtr<WorkerRunnable> runnable = std::move(pending[i]); + nsresult rv = DispatchLockHeld(runnable.forget(), nullptr, lock); + NS_ENSURE_SUCCESS(rv, rv); + } + MOZ_RELEASE_ASSERT(mDelayedDebuggeeRunnables.IsEmpty()); + } + + return NS_OK; +} + +// static +nsresult WorkerPrivate::GetLoadInfo( + JSContext* aCx, nsPIDOMWindowInner* aWindow, WorkerPrivate* aParent, + const nsAString& aScriptURL, const enum WorkerType& aWorkerType, + const RequestCredentials& aCredentials, bool aIsChromeWorker, + LoadGroupBehavior aLoadGroupBehavior, WorkerKind aWorkerKind, + WorkerLoadInfo* aLoadInfo) { + using namespace mozilla::dom::workerinternals; + + MOZ_ASSERT(aCx); + MOZ_ASSERT_IF(NS_IsMainThread(), + aCx == nsContentUtils::GetCurrentJSContext()); + + if (aWindow) { + AssertIsOnMainThread(); + } + + WorkerLoadInfo loadInfo; + nsresult rv; + + if (aParent) { + aParent->AssertIsOnWorkerThread(); + + // If the parent is going away give up now. + WorkerStatus parentStatus; + { + MutexAutoLock lock(aParent->mMutex); + parentStatus = aParent->mStatus; + } + + if (parentStatus > Running) { + return NS_ERROR_FAILURE; + } + + // Passing a pointer to our stack loadInfo is safe here because this + // method uses a sync runnable to get the channel from the main thread. + rv = ChannelFromScriptURLWorkerThread(aCx, aParent, aScriptURL, aWorkerType, + aCredentials, loadInfo); + if (NS_FAILED(rv)) { + MOZ_ALWAYS_TRUE(loadInfo.ProxyReleaseMainThreadObjects(aParent)); + return rv; + } + + // Now that we've spun the loop there's no guarantee that our parent is + // still alive. We may have received control messages initiating shutdown. + { + MutexAutoLock lock(aParent->mMutex); + parentStatus = aParent->mStatus; + } + + if (parentStatus > Running) { + MOZ_ALWAYS_TRUE(loadInfo.ProxyReleaseMainThreadObjects(aParent)); + return NS_ERROR_FAILURE; + } + + loadInfo.mTrials = aParent->Trials(); + loadInfo.mDomain = aParent->Domain(); + loadInfo.mFromWindow = aParent->IsFromWindow(); + loadInfo.mWindowID = aParent->WindowID(); + loadInfo.mAssociatedBrowsingContextID = + aParent->AssociatedBrowsingContextID(); + loadInfo.mStorageAccess = aParent->StorageAccess(); + loadInfo.mUseRegularPrincipal = aParent->UseRegularPrincipal(); + loadInfo.mUsingStorageAccess = aParent->UsingStorageAccess(); + loadInfo.mCookieJarSettings = aParent->CookieJarSettings(); + if (loadInfo.mCookieJarSettings) { + loadInfo.mCookieJarSettingsArgs = aParent->CookieJarSettingsArgs(); + } + loadInfo.mOriginAttributes = aParent->GetOriginAttributes(); + loadInfo.mServiceWorkersTestingInWindow = + aParent->ServiceWorkersTestingInWindow(); + loadInfo.mIsThirdPartyContextToTopWindow = + aParent->IsThirdPartyContextToTopWindow(); + loadInfo.mShouldResistFingerprinting = aParent->ShouldResistFingerprinting( + RFPTarget::IsAlwaysEnabledForPrecompute); + loadInfo.mOverriddenFingerprintingSettings = + aParent->GetOverriddenFingerprintingSettings(); + loadInfo.mParentController = aParent->GlobalScope()->GetController(); + loadInfo.mWatchedByDevTools = aParent->IsWatchedByDevTools(); + } else { + AssertIsOnMainThread(); + + // Make sure that the IndexedDatabaseManager is set up + Unused << NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate()); + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + MOZ_ASSERT(ssm); + + bool isChrome = nsContentUtils::IsSystemCaller(aCx); + + // First check to make sure the caller has permission to make a privileged + // worker if they called the ChromeWorker/ChromeSharedWorker constructor. + if (aIsChromeWorker && !isChrome) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Chrome callers (whether creating a ChromeWorker or Worker) always get the + // system principal here as they're allowed to load anything. The script + // loader will refuse to run any script that does not also have the system + // principal. + if (isChrome) { + rv = ssm->GetSystemPrincipal(getter_AddRefs(loadInfo.mLoadingPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // See if we're being called from a window. + nsCOMPtr<nsPIDOMWindowInner> globalWindow = aWindow; + if (!globalWindow) { + globalWindow = xpc::CurrentWindowOrNull(aCx); + } + + nsCOMPtr<Document> document; + Maybe<ClientInfo> clientInfo; + + if (globalWindow) { + // Only use the current inner window, and only use it if the caller can + // access it. + if (nsPIDOMWindowOuter* outerWindow = globalWindow->GetOuterWindow()) { + loadInfo.mWindow = outerWindow->GetCurrentInnerWindow(); + } + + loadInfo.mTrials = + OriginTrials::FromWindow(nsGlobalWindowInner::Cast(loadInfo.mWindow)); + + BrowsingContext* browsingContext = globalWindow->GetBrowsingContext(); + + // TODO: fix this for SharedWorkers with multiple documents (bug + // 1177935) + loadInfo.mServiceWorkersTestingInWindow = + browsingContext && + browsingContext->Top()->ServiceWorkersTestingEnabled(); + + if (!loadInfo.mWindow || + (globalWindow != loadInfo.mWindow && + !nsContentUtils::CanCallerAccess(loadInfo.mWindow))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(loadInfo.mWindow); + MOZ_ASSERT(sgo); + + loadInfo.mScriptContext = sgo->GetContext(); + NS_ENSURE_TRUE(loadInfo.mScriptContext, NS_ERROR_FAILURE); + + // If we're called from a window then we can dig out the principal and URI + // from the document. + document = loadInfo.mWindow->GetExtantDoc(); + NS_ENSURE_TRUE(document, NS_ERROR_FAILURE); + + loadInfo.mBaseURI = document->GetDocBaseURI(); + loadInfo.mLoadGroup = document->GetDocumentLoadGroup(); + NS_ENSURE_TRUE(loadInfo.mLoadGroup, NS_ERROR_FAILURE); + + clientInfo = globalWindow->GetClientInfo(); + + // Use the document's NodePrincipal as loading principal if we're not + // being called from chrome. + if (!loadInfo.mLoadingPrincipal) { + loadInfo.mLoadingPrincipal = document->NodePrincipal(); + NS_ENSURE_TRUE(loadInfo.mLoadingPrincipal, NS_ERROR_FAILURE); + + // We use the document's base domain to limit the number of workers + // each domain can create. For sandboxed documents, we use the domain + // of their first non-sandboxed document, walking up until we find + // one. If we can't find one, we fall back to using the GUID of the + // null principal as the base domain. + if (document->GetSandboxFlags() & SANDBOXED_ORIGIN) { + nsCOMPtr<Document> tmpDoc = document; + do { + tmpDoc = tmpDoc->GetInProcessParentDocument(); + } while (tmpDoc && tmpDoc->GetSandboxFlags() & SANDBOXED_ORIGIN); + + if (tmpDoc) { + // There was an unsandboxed ancestor, yay! + nsCOMPtr<nsIPrincipal> tmpPrincipal = tmpDoc->NodePrincipal(); + rv = tmpPrincipal->GetBaseDomain(loadInfo.mDomain); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // No unsandboxed ancestor, use our GUID. + rv = loadInfo.mLoadingPrincipal->GetBaseDomain(loadInfo.mDomain); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Document creating the worker is not sandboxed. + rv = loadInfo.mLoadingPrincipal->GetBaseDomain(loadInfo.mDomain); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + NS_ENSURE_TRUE(NS_LoadGroupMatchesPrincipal(loadInfo.mLoadGroup, + loadInfo.mLoadingPrincipal), + NS_ERROR_FAILURE); + + nsCOMPtr<nsIPermissionManager> permMgr = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t perm; + rv = permMgr->TestPermissionFromPrincipal(loadInfo.mLoadingPrincipal, + "systemXHR"_ns, &perm); + NS_ENSURE_SUCCESS(rv, rv); + + loadInfo.mXHRParamsAllowed = perm == nsIPermissionManager::ALLOW_ACTION; + + loadInfo.mWatchedByDevTools = + browsingContext && browsingContext->WatchedByDevTools(); + + loadInfo.mReferrerInfo = + ReferrerInfo::CreateForFetch(loadInfo.mLoadingPrincipal, document); + loadInfo.mFromWindow = true; + loadInfo.mWindowID = globalWindow->WindowID(); + loadInfo.mAssociatedBrowsingContextID = + globalWindow->GetBrowsingContext()->Id(); + loadInfo.mStorageAccess = StorageAllowedForWindow(globalWindow); + loadInfo.mUseRegularPrincipal = document->UseRegularPrincipal(); + loadInfo.mUsingStorageAccess = document->UsingStorageAccess(); + loadInfo.mShouldResistFingerprinting = + document->ShouldResistFingerprinting( + RFPTarget::IsAlwaysEnabledForPrecompute); + loadInfo.mOverriddenFingerprintingSettings = + document->GetOverriddenFingerprintingSettings(); + + // This is an hack to deny the storage-access-permission for workers of + // sub-iframes. + if (loadInfo.mUsingStorageAccess && + StorageAllowedForDocument(document) != StorageAccess::eAllow) { + loadInfo.mUsingStorageAccess = false; + } + loadInfo.mIsThirdPartyContextToTopWindow = + AntiTrackingUtils::IsThirdPartyWindow(globalWindow, nullptr); + loadInfo.mCookieJarSettings = document->CookieJarSettings(); + if (loadInfo.mCookieJarSettings) { + auto* cookieJarSettings = + net::CookieJarSettings::Cast(loadInfo.mCookieJarSettings); + cookieJarSettings->Serialize(loadInfo.mCookieJarSettingsArgs); + } + StoragePrincipalHelper::GetRegularPrincipalOriginAttributes( + document, loadInfo.mOriginAttributes); + loadInfo.mParentController = globalWindow->GetController(); + loadInfo.mSecureContext = loadInfo.mWindow->IsSecureContext() + ? WorkerLoadInfo::eSecureContext + : WorkerLoadInfo::eInsecureContext; + } else { + // Not a window + MOZ_ASSERT(isChrome); + + // We're being created outside of a window. Need to figure out the script + // that is creating us in order for us to use relative URIs later on. + JS::AutoFilename fileName; + if (JS::DescribeScriptedCaller(aCx, &fileName)) { + // In most cases, fileName is URI. In a few other cases + // (e.g. xpcshell), fileName is a file path. Ideally, we would + // prefer testing whether fileName parses as an URI and fallback + // to file path in case of error, but Windows file paths have + // the interesting property that they can be parsed as bogus + // URIs (e.g. C:/Windows/Tmp is interpreted as scheme "C", + // hostname "Windows", path "Tmp"), which defeats this algorithm. + // Therefore, we adopt the opposite convention. + nsCOMPtr<nsIFile> scriptFile = + do_CreateInstance("@mozilla.org/file/local;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + rv = scriptFile->InitWithPath(NS_ConvertUTF8toUTF16(fileName.get())); + if (NS_SUCCEEDED(rv)) { + rv = NS_NewFileURI(getter_AddRefs(loadInfo.mBaseURI), scriptFile); + } + if (NS_FAILED(rv)) { + // As expected, fileName is not a path, so proceed with + // a uri. + rv = NS_NewURI(getter_AddRefs(loadInfo.mBaseURI), fileName.get()); + } + if (NS_FAILED(rv)) { + return rv; + } + } + loadInfo.mXHRParamsAllowed = true; + loadInfo.mFromWindow = false; + loadInfo.mWindowID = UINT64_MAX; + loadInfo.mStorageAccess = StorageAccess::eAllow; + loadInfo.mUseRegularPrincipal = true; + loadInfo.mUsingStorageAccess = false; + loadInfo.mCookieJarSettings = + mozilla::net::CookieJarSettings::Create(loadInfo.mLoadingPrincipal); + loadInfo.mShouldResistFingerprinting = + nsContentUtils::ShouldResistFingerprinting_dangerous( + loadInfo.mLoadingPrincipal, + "Unusual situation - we have no document or CookieJarSettings", + RFPTarget::IsAlwaysEnabledForPrecompute); + MOZ_ASSERT(loadInfo.mCookieJarSettings); + auto* cookieJarSettings = + net::CookieJarSettings::Cast(loadInfo.mCookieJarSettings); + cookieJarSettings->Serialize(loadInfo.mCookieJarSettingsArgs); + + loadInfo.mOriginAttributes = OriginAttributes(); + loadInfo.mIsThirdPartyContextToTopWindow = false; + } + + MOZ_ASSERT(loadInfo.mLoadingPrincipal); + MOZ_ASSERT(isChrome || !loadInfo.mDomain.IsEmpty()); + + if (!loadInfo.mLoadGroup || aLoadGroupBehavior == OverrideLoadGroup) { + OverrideLoadInfoLoadGroup(loadInfo, loadInfo.mLoadingPrincipal); + } + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadInfo.mLoadGroup, + loadInfo.mLoadingPrincipal)); + + // Top level workers' main script use the document charset for the script + // uri encoding. + nsCOMPtr<nsIURI> url; + rv = nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(url), aScriptURL, document, loadInfo.mBaseURI); + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); + + rv = ChannelFromScriptURLMainThread( + loadInfo.mLoadingPrincipal, document, loadInfo.mLoadGroup, url, + aWorkerType, aCredentials, clientInfo, ContentPolicyType(aWorkerKind), + loadInfo.mCookieJarSettings, loadInfo.mReferrerInfo, + getter_AddRefs(loadInfo.mChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_GetFinalChannelURI(loadInfo.mChannel, + getter_AddRefs(loadInfo.mResolvedScriptURI)); + NS_ENSURE_SUCCESS(rv, rv); + + // We need the correct hasStoragePermission flag for the channel here since + // we will do a content blocking check later when we set the storage + // principal for the worker. The channel here won't be opened when we do the + // check later, so the hasStoragePermission flag is incorrect. To address + // this, We copy the hasStoragePermission flag from the document if there is + // a window. The worker is created as the same origin of the window. So, the + // worker is supposed to have the same storage permission as the window as + // well as the hasStoragePermission flag. + nsCOMPtr<nsILoadInfo> channelLoadInfo = loadInfo.mChannel->LoadInfo(); + rv = channelLoadInfo->SetStoragePermission( + loadInfo.mUsingStorageAccess ? nsILoadInfo::HasStoragePermission + : nsILoadInfo::NoStoragePermission); + NS_ENSURE_SUCCESS(rv, rv); + + rv = loadInfo.SetPrincipalsAndCSPFromChannel(loadInfo.mChannel); + NS_ENSURE_SUCCESS(rv, rv); + } + + MOZ_DIAGNOSTIC_ASSERT(loadInfo.mLoadingPrincipal); + MOZ_DIAGNOSTIC_ASSERT(loadInfo.PrincipalIsValid()); + + *aLoadInfo = std::move(loadInfo); + return NS_OK; +} + +// static +void WorkerPrivate::OverrideLoadInfoLoadGroup(WorkerLoadInfo& aLoadInfo, + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(!aLoadInfo.mInterfaceRequestor); + MOZ_ASSERT(aLoadInfo.mLoadingPrincipal == aPrincipal); + + aLoadInfo.mInterfaceRequestor = + new WorkerLoadInfo::InterfaceRequestor(aPrincipal, aLoadInfo.mLoadGroup); + aLoadInfo.mInterfaceRequestor->MaybeAddBrowserChild(aLoadInfo.mLoadGroup); + + // NOTE: this defaults the load context to: + // - private browsing = false + // - content = true + // - use remote tabs = false + nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); + + nsresult rv = + loadGroup->SetNotificationCallbacks(aLoadInfo.mInterfaceRequestor); + MOZ_ALWAYS_SUCCEEDS(rv); + + aLoadInfo.mLoadGroup = std::move(loadGroup); + + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(aLoadInfo.mLoadGroup, aPrincipal)); +} + +void WorkerPrivate::RunLoopNeverRan() { + LOG(WorkerLog(), ("WorkerPrivate::RunLoopNeverRan [%p]", this)); + { + MutexAutoLock lock(mMutex); + + mStatus = Dead; + } + + // After mStatus is set to Dead there can be no more + // WorkerControlRunnables so no need to lock here. + if (!mControlQueue.IsEmpty()) { + WorkerControlRunnable* runnable = nullptr; + while (mControlQueue.Pop(runnable)) { + runnable->Cancel(); + runnable->Release(); + } + } + + // There should be no StrongWorkerRefs, child Workers, and Timeouts, but + // WeakWorkerRefs could. WorkerThreadPrimaryRunnable could have created a + // PerformanceStorageWorker which holds a WeakWorkerRef. + // Notify WeakWorkerRefs with Dead status. + NotifyWorkerRefs(Dead); + + ScheduleDeletion(WorkerPrivate::WorkerRan); +} + +void WorkerPrivate::UnrootGlobalScopes() { + LOG(WorkerLog(), ("WorkerPrivate::UnrootGlobalScopes [%p]", this)); + auto data = mWorkerThreadAccessible.Access(); + + RefPtr<WorkerDebuggerGlobalScope> debugScope = data->mDebuggerScope.forget(); + if (debugScope) { + MOZ_ASSERT(debugScope->mWorkerPrivate == this); + } + RefPtr<WorkerGlobalScope> scope = data->mScope.forget(); + if (scope) { + MOZ_ASSERT(scope->mWorkerPrivate == this); + } +} + +void WorkerPrivate::DoRunLoop(JSContext* aCx) { + LOG(WorkerLog(), ("WorkerPrivate::DoRunLoop [%p]", this)); + auto data = mWorkerThreadAccessible.Access(); + MOZ_RELEASE_ASSERT(!GetExecutionManager()); + + RefPtr<WorkerThread> thread; + { + MutexAutoLock lock(mMutex); + mJSContext = aCx; + // mThread is set before we enter, and is never changed during DoRunLoop. + // copy to local so we don't trigger mutex analysis + MOZ_ASSERT(mThread); + thread = mThread; + + MOZ_ASSERT(mStatus == Pending); + mStatus = Running; + } + + // Now that we've done that, we can go ahead and set up our AutoJSAPI. We + // can't before this point, because it can't find the right JSContext before + // then, since it gets it from our mJSContext. + AutoJSAPI jsapi; + jsapi.Init(); + MOZ_ASSERT(jsapi.cx() == aCx); + + EnableMemoryReporter(); + + InitializeGCTimers(); + + bool checkFinalGCCC = + StaticPrefs::dom_workers_GCCC_on_potentially_last_event(); + + bool debuggerRunnablesPending = false; + bool normalRunnablesPending = false; + auto noRunnablesPendingAndKeepAlive = + [&debuggerRunnablesPending, &normalRunnablesPending, &thread, this]() + MOZ_REQUIRES(mMutex) { + // We want to keep both pending flags always updated while looping. + debuggerRunnablesPending = !mDebuggerQueue.IsEmpty(); + normalRunnablesPending = NS_HasPendingEvents(thread); + + bool anyRunnablesPending = !mControlQueue.IsEmpty() || + debuggerRunnablesPending || + normalRunnablesPending; + bool keepWorkerAlive = mStatus == Running || HasActiveWorkerRefs(); + + return (!anyRunnablesPending && keepWorkerAlive); + }; + + for (;;) { + WorkerStatus currentStatus; + + if (checkFinalGCCC) { + // If we get here after the last event ran but someone holds a WorkerRef + // and there is no other logic to release that WorkerRef than lazily + // through GC/CC, we might block forever on the next WaitForWorkerEvents. + // Every object holding a WorkerRef should really have a straight, + // deterministic line from the WorkerRef's callback being invoked to the + // WorkerRef being released which is supported by strong-references that + // can't form a cycle. + bool mayNeedFinalGCCC = false; + { + MutexAutoLock lock(mMutex); + + currentStatus = mStatus; + mayNeedFinalGCCC = + (mStatus >= Canceling && HasActiveWorkerRefs() && + !debuggerRunnablesPending && !normalRunnablesPending && + data->mPerformedShutdownAfterLastContentTaskExecuted); + } + if (mayNeedFinalGCCC) { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + // WorkerRef::ReleaseWorker will check this flag via + // AssertIsNotPotentiallyLastGCCCRunning + data->mIsPotentiallyLastGCCCRunning = true; +#endif + // GarbageCollectInternal will trigger both GC and CC + GarbageCollectInternal(aCx, true /* aShrinking */, + true /* aCollectChildren */); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + data->mIsPotentiallyLastGCCCRunning = false; +#endif + } + } + + { + MutexAutoLock lock(mMutex); + + LOGV( + ("WorkerPrivate::DoRunLoop [%p] mStatus %u before getting events" + " to run", + this, (uint8_t)mStatus)); + if (checkFinalGCCC && currentStatus != mStatus) { + // Something moved our status while we were supposed to check for a + // potentially needed GC/CC. Just check again. + continue; + } + + // Wait for a runnable to arrive that we can execute, or for it to be okay + // to shutdown this worker once all holders have been removed. + // Holders may be removed from inside normal runnables, but we don't + // check for that after processing normal runnables, so we need to let + // control flow to the shutdown logic without blocking. + while (noRunnablesPendingAndKeepAlive()) { + // We pop out to this loop when there are no pending events. + // If we don't reset these, we may not re-enter ProcessNextEvent() + // until we have events to process, and it may seem like we have + // an event running for a very long time. + thread->SetRunningEventDelay(TimeDuration(), TimeStamp()); + + mWorkerLoopIsIdle = true; + + WaitForWorkerEvents(); + + mWorkerLoopIsIdle = false; + } + + auto result = ProcessAllControlRunnablesLocked(); + if (result != ProcessAllControlRunnablesResult::Nothing) { + // Update all saved runnable flags for side effect for the + // loop check about transitioning to Killing below. + (void)noRunnablesPendingAndKeepAlive(); + } + + currentStatus = mStatus; + } + + // Transition from Canceling to Killing and exit this loop when: + // * All (non-weak) WorkerRefs have been released. + // * There are no runnables pending. This is intended to let same-thread + // dispatches as part of cleanup be able to run to completion, but any + // logic that still wants async things to happen should be holding a + // StrongWorkerRef. + if (currentStatus != Running && !HasActiveWorkerRefs() && + !normalRunnablesPending && !debuggerRunnablesPending) { + // Now we are ready to kill the worker thread. + if (currentStatus == Canceling) { + NotifyInternal(Killing); + +#ifdef DEBUG + { + MutexAutoLock lock(mMutex); + currentStatus = mStatus; + } + MOZ_ASSERT(currentStatus == Killing); +#else + currentStatus = Killing; +#endif + } + + // If we're supposed to die then we should exit the loop. + if (currentStatus == Killing) { + // We are about to destroy worker, report all use counters. + ReportUseCounters(); + + // Flush uncaught rejections immediately, without + // waiting for a next tick. + PromiseDebugging::FlushUncaughtRejections(); + + ShutdownGCTimers(); + + DisableMemoryReporter(); + + { + MutexAutoLock lock(mMutex); + + mStatus = Dead; + mJSContext = nullptr; + } + + // After mStatus is set to Dead there can be no more + // WorkerControlRunnables so no need to lock here. + if (!mControlQueue.IsEmpty()) { + LOG(WorkerLog(), + ("WorkerPrivate::DoRunLoop [%p] dropping control runnables in " + "Dead status", + this)); + WorkerControlRunnable* runnable = nullptr; + while (mControlQueue.Pop(runnable)) { + runnable->Cancel(); + runnable->Release(); + } + } + + // We do not need the timeouts any more, they have been canceled + // by NotifyInternal(Killing) above if they were active. + UnlinkTimeouts(); + + return; + } + } + + // Status transitions to Closing/Canceling and there are no SyncLoops, + // set global start dying, disconnect EventTargetObjects and + // WebTaskScheduler. + if (currentStatus >= Closing && + !data->mPerformedShutdownAfterLastContentTaskExecuted) { + data->mPerformedShutdownAfterLastContentTaskExecuted.Flip(); + if (data->mScope) { + data->mScope->NoteTerminating(); + data->mScope->DisconnectGlobalTeardownObservers(); + if (data->mScope->GetExistingScheduler()) { + data->mScope->GetExistingScheduler()->Disconnect(); + } + } + } + + if (debuggerRunnablesPending || normalRunnablesPending) { + // Start the periodic GC timer if it is not already running. + SetGCTimerMode(PeriodicTimer); + } + + if (debuggerRunnablesPending) { + WorkerRunnable* runnable = nullptr; + + { + MutexAutoLock lock(mMutex); + + mDebuggerQueue.Pop(runnable); + debuggerRunnablesPending = !mDebuggerQueue.IsEmpty(); + } + + { + MOZ_ASSERT(runnable); + AUTO_PROFILE_FOLLOWING_RUNNABLE(runnable); + static_cast<nsIRunnable*>(runnable)->Run(); + } + runnable->Release(); + + CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get(); + ccjs->PerformDebuggerMicroTaskCheckpoint(); + + if (debuggerRunnablesPending) { + WorkerDebuggerGlobalScope* globalScope = DebuggerGlobalScope(); + // If the worker was canceled before ever creating its content global + // then mCancelBeforeWorkerScopeConstructed could have been flipped and + // all of the WorkerDebuggerRunnables canceled, so the debugger global + // would never have been created. + if (globalScope) { + // Now *might* be a good time to GC. Let the JS engine make the + // decision. + JSAutoRealm ar(aCx, globalScope->GetGlobalJSObject()); + JS_MaybeGC(aCx); + } + } + } else if (normalRunnablesPending) { + // Process a single runnable from the main queue. + NS_ProcessNextEvent(thread, false); + + normalRunnablesPending = NS_HasPendingEvents(thread); + if (normalRunnablesPending && GlobalScope()) { + // Now *might* be a good time to GC. Let the JS engine make the + // decision. + JSAutoRealm ar(aCx, GlobalScope()->GetGlobalJSObject()); + JS_MaybeGC(aCx); + } + } + + // Checking the background actors if needed, any runnable execution could + // release background actors which blocks GC/CC on + // WorkerPrivate::mParentEventTargetRef. + if (currentStatus < Canceling) { + UpdateCCFlag(CCFlag::CheckBackgroundActors); + } + + if (!debuggerRunnablesPending && !normalRunnablesPending) { + // Both the debugger event queue and the normal event queue has been + // exhausted, cancel the periodic GC timer and schedule the idle GC timer. + SetGCTimerMode(IdleTimer); + } + + // If the worker thread is spamming the main thread faster than it can + // process the work, then pause the worker thread until the main thread + // catches up. + size_t queuedEvents = mMainThreadEventTargetForMessaging->Length() + + mMainThreadDebuggeeEventTarget->Length(); + if (queuedEvents > 5000) { + // Note, postMessage uses mMainThreadDebuggeeEventTarget! + mMainThreadDebuggeeEventTarget->AwaitIdle(); + } + } + + MOZ_CRASH("Shouldn't get here!"); +} + +namespace { +/** + * If there is a current CycleCollectedJSContext, return its recursion depth, + * otherwise return 1. + * + * In the edge case where a worker is starting up so late that PBackground is + * already shutting down, the cycle collected context will never be created, + * but we will need to drain the event loop in ClearMainEventQueue. This will + * result in a normal NS_ProcessPendingEvents invocation which will call + * WorkerPrivate::OnProcessNextEvent and WorkerPrivate::AfterProcessNextEvent + * which want to handle the need to process control runnables and perform a + * sanity check assertion, respectively. + * + * We claim a depth of 1 when there's no CCJS because this most corresponds to + * reality, but this doesn't meant that other code might want to drain various + * runnable queues as part of this cleanup. + */ +uint32_t GetEffectiveEventLoopRecursionDepth() { + auto* ccjs = CycleCollectedJSContext::Get(); + if (ccjs) { + return ccjs->RecursionDepth(); + } + + return 1; +} + +} // namespace + +void WorkerPrivate::OnProcessNextEvent() { + AssertIsOnWorkerThread(); + + uint32_t recursionDepth = GetEffectiveEventLoopRecursionDepth(); + MOZ_ASSERT(recursionDepth); + + // Normally we process control runnables in DoRunLoop or RunCurrentSyncLoop. + // However, it's possible that non-worker C++ could spin its own nested event + // loop, and in that case we must ensure that we continue to process control + // runnables here. + if (recursionDepth > 1 && mSyncLoopStack.Length() < recursionDepth - 1) { + Unused << ProcessAllControlRunnables(); + // There's no running JS, and no state to revalidate, so we can ignore the + // return value. + } +} + +void WorkerPrivate::AfterProcessNextEvent() { + AssertIsOnWorkerThread(); + MOZ_ASSERT(GetEffectiveEventLoopRecursionDepth()); +} + +nsISerialEventTarget* WorkerPrivate::MainThreadEventTargetForMessaging() { + return mMainThreadEventTargetForMessaging; +} + +nsresult WorkerPrivate::DispatchToMainThreadForMessaging(nsIRunnable* aRunnable, + uint32_t aFlags) { + nsCOMPtr<nsIRunnable> r = aRunnable; + return DispatchToMainThreadForMessaging(r.forget(), aFlags); +} + +nsresult WorkerPrivate::DispatchToMainThreadForMessaging( + already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) { + return mMainThreadEventTargetForMessaging->Dispatch(std::move(aRunnable), + aFlags); +} + +nsISerialEventTarget* WorkerPrivate::MainThreadEventTarget() { + return mMainThreadEventTarget; +} + +nsresult WorkerPrivate::DispatchToMainThread(nsIRunnable* aRunnable, + uint32_t aFlags) { + nsCOMPtr<nsIRunnable> r = aRunnable; + return DispatchToMainThread(r.forget(), aFlags); +} + +nsresult WorkerPrivate::DispatchToMainThread( + already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) { + return mMainThreadEventTarget->Dispatch(std::move(aRunnable), aFlags); +} + +nsresult WorkerPrivate::DispatchDebuggeeToMainThread( + already_AddRefed<WorkerDebuggeeRunnable> aRunnable, uint32_t aFlags) { + return mMainThreadDebuggeeEventTarget->Dispatch(std::move(aRunnable), aFlags); +} + +nsISerialEventTarget* WorkerPrivate::ControlEventTarget() { + return mWorkerControlEventTarget; +} + +nsISerialEventTarget* WorkerPrivate::HybridEventTarget() { + return mWorkerHybridEventTarget; +} + +ClientType WorkerPrivate::GetClientType() const { + switch (Kind()) { + case WorkerKindDedicated: + return ClientType::Worker; + case WorkerKindShared: + return ClientType::Sharedworker; + case WorkerKindService: + return ClientType::Serviceworker; + default: + MOZ_CRASH("unknown worker type!"); + } +} + +UniquePtr<ClientSource> WorkerPrivate::CreateClientSource() { + auto data = mWorkerThreadAccessible.Access(); + MOZ_ASSERT(!data->mScope, "Client should be created before the global"); + + auto clientSource = ClientManager::CreateSource( + GetClientType(), mWorkerHybridEventTarget, + StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker(this) + ? GetPartitionedPrincipalInfo() + : GetPrincipalInfo()); + MOZ_DIAGNOSTIC_ASSERT(clientSource); + + clientSource->SetAgentClusterId(mAgentClusterId); + + if (data->mFrozen) { + clientSource->Freeze(); + } + + // Shortly after the client is reserved we will try loading the main script + // for the worker. This may get intercepted by the ServiceWorkerManager + // which will then try to create a ClientHandle. Its actually possible for + // the main thread to create this ClientHandle before our IPC message creating + // the ClientSource completes. To avoid this race we synchronously ping our + // parent Client actor here. This ensure the worker ClientSource is created + // in the parent before the main thread might try reaching it with a + // ClientHandle. + // + // An alternative solution would have been to handle the out-of-order + // operations on the parent side. We could have created a small window where + // we allow ClientHandle objects to exist without a ClientSource. We would + // then time out these handles if they stayed orphaned for too long. This + // approach would be much more complex, but also avoid this extra bit of + // latency when starting workers. + // + // Note, we only have to do this for workers that can be controlled by a + // service worker. So avoid the sync overhead here if we are starting a + // service worker or a chrome worker. + if (Kind() != WorkerKindService && !IsChromeWorker()) { + clientSource->WorkerSyncPing(this); + } + + return clientSource; +} + +bool WorkerPrivate::EnsureCSPEventListener() { + if (!mCSPEventListener) { + mCSPEventListener = WorkerCSPEventListener::Create(this); + if (NS_WARN_IF(!mCSPEventListener)) { + return false; + } + } + return true; +} + +nsICSPEventListener* WorkerPrivate::CSPEventListener() const { + MOZ_ASSERT(mCSPEventListener); + return mCSPEventListener; +} + +void WorkerPrivate::EnsurePerformanceStorage() { + AssertIsOnWorkerThread(); + + if (!mPerformanceStorage) { + mPerformanceStorage = PerformanceStorageWorker::Create(this); + } +} + +bool WorkerPrivate::GetExecutionGranted() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mJSThreadExecutionGranted; +} + +void WorkerPrivate::SetExecutionGranted(bool aGranted) { + auto data = mWorkerThreadAccessible.Access(); + data->mJSThreadExecutionGranted = aGranted; +} + +void WorkerPrivate::ScheduleTimeSliceExpiration(uint32_t aDelay) { + auto data = mWorkerThreadAccessible.Access(); + + if (!data->mTSTimer) { + data->mTSTimer = NS_NewTimer(); + MOZ_ALWAYS_SUCCEEDS(data->mTSTimer->SetTarget(mWorkerControlEventTarget)); + } + + // Whenever an event is scheduled on the WorkerControlEventTarget an + // interrupt is automatically requested which causes us to yield JS execution + // and the next JS execution in the queue to execute. + // This allows for simple code reuse of the existing interrupt callback code + // used for control events. + MOZ_ALWAYS_SUCCEEDS(data->mTSTimer->InitWithNamedFuncCallback( + [](nsITimer* Timer, void* aClosure) { return; }, nullptr, aDelay, + nsITimer::TYPE_ONE_SHOT, "TimeSliceExpirationTimer")); +} + +void WorkerPrivate::CancelTimeSliceExpiration() { + auto data = mWorkerThreadAccessible.Access(); + MOZ_ALWAYS_SUCCEEDS(data->mTSTimer->Cancel()); +} + +JSExecutionManager* WorkerPrivate::GetExecutionManager() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mExecutionManager.get(); +} + +void WorkerPrivate::SetExecutionManager(JSExecutionManager* aManager) { + auto data = mWorkerThreadAccessible.Access(); + data->mExecutionManager = aManager; +} + +void WorkerPrivate::ExecutionReady() { + auto data = mWorkerThreadAccessible.Access(); + { + MutexAutoLock lock(mMutex); + if (mStatus >= Canceling) { + return; + } + } + + data->mScope->MutableClientSourceRef().WorkerExecutionReady(this); + + if (ExtensionAPIAllowed()) { + extensions::CreateAndDispatchInitWorkerContextRunnable(); + } +} + +void WorkerPrivate::InitializeGCTimers() { + auto data = mWorkerThreadAccessible.Access(); + + // We need timers for GC. The basic plan is to run a non-shrinking GC + // periodically (PERIODIC_GC_TIMER_DELAY_SEC) while the worker is running. + // Once the worker goes idle we set a short (IDLE_GC_TIMER_DELAY_SEC) timer to + // run a shrinking GC. + data->mPeriodicGCTimer = NS_NewTimer(); + data->mIdleGCTimer = NS_NewTimer(); + + data->mPeriodicGCTimerRunning = false; + data->mIdleGCTimerRunning = false; +} + +void WorkerPrivate::SetGCTimerMode(GCTimerMode aMode) { + auto data = mWorkerThreadAccessible.Access(); + + if (!data->mPeriodicGCTimer || !data->mIdleGCTimer) { + // GC timers have been cleared already. + return; + } + + if (aMode == NoTimer) { + MOZ_ALWAYS_SUCCEEDS(data->mPeriodicGCTimer->Cancel()); + data->mPeriodicGCTimerRunning = false; + MOZ_ALWAYS_SUCCEEDS(data->mIdleGCTimer->Cancel()); + data->mIdleGCTimerRunning = false; + return; + } + + WorkerStatus status; + { + MutexAutoLock lock(mMutex); + status = mStatus; + } + + if (status >= Killing) { + ShutdownGCTimers(); + return; + } + + // If the idle timer is running, don't cancel it when the periodic timer + // is scheduled since we do want shrinking GC to be called occasionally. + if (aMode == PeriodicTimer && data->mPeriodicGCTimerRunning) { + return; + } + + if (aMode == IdleTimer) { + if (!data->mPeriodicGCTimerRunning) { + // Since running idle GC cancels both GC timers, after that we want + // first at least periodic GC timer getting activated, since that tells + // us that there have been some non-control tasks to process. Otherwise + // idle GC timer would keep running all the time. + return; + } + + // Cancel the periodic timer now, since the event loop is (in the common + // case) empty now. + MOZ_ALWAYS_SUCCEEDS(data->mPeriodicGCTimer->Cancel()); + data->mPeriodicGCTimerRunning = false; + + if (data->mIdleGCTimerRunning) { + return; + } + } + + MOZ_ASSERT(aMode == PeriodicTimer || aMode == IdleTimer); + + uint32_t delay = 0; + int16_t type = nsITimer::TYPE_ONE_SHOT; + nsTimerCallbackFunc callback = nullptr; + const char* name = nullptr; + nsITimer* timer = nullptr; + + if (aMode == PeriodicTimer) { + delay = PERIODIC_GC_TIMER_DELAY_SEC * 1000; + type = nsITimer::TYPE_REPEATING_SLACK; + callback = PeriodicGCTimerCallback; + name = "dom::PeriodicGCTimerCallback"; + timer = data->mPeriodicGCTimer; + data->mPeriodicGCTimerRunning = true; + LOG(WorkerLog(), ("Worker %p scheduled periodic GC timer\n", this)); + } else { + delay = IDLE_GC_TIMER_DELAY_SEC * 1000; + type = nsITimer::TYPE_ONE_SHOT; + callback = IdleGCTimerCallback; + name = "dom::IdleGCTimerCallback"; + timer = data->mIdleGCTimer; + data->mIdleGCTimerRunning = true; + LOG(WorkerLog(), ("Worker %p scheduled idle GC timer\n", this)); + } + + MOZ_ALWAYS_SUCCEEDS(timer->SetTarget(mWorkerControlEventTarget)); + MOZ_ALWAYS_SUCCEEDS( + timer->InitWithNamedFuncCallback(callback, this, delay, type, name)); +} + +void WorkerPrivate::ShutdownGCTimers() { + auto data = mWorkerThreadAccessible.Access(); + + MOZ_ASSERT(!data->mPeriodicGCTimer == !data->mIdleGCTimer); + + if (!data->mPeriodicGCTimer && !data->mIdleGCTimer) { + return; + } + + // Always make sure the timers are canceled. + MOZ_ALWAYS_SUCCEEDS(data->mPeriodicGCTimer->Cancel()); + MOZ_ALWAYS_SUCCEEDS(data->mIdleGCTimer->Cancel()); + + LOG(WorkerLog(), ("Worker %p killed the GC timers\n", this)); + + data->mPeriodicGCTimer = nullptr; + data->mIdleGCTimer = nullptr; + data->mPeriodicGCTimerRunning = false; + data->mIdleGCTimerRunning = false; +} + +bool WorkerPrivate::InterruptCallback(JSContext* aCx) { + auto data = mWorkerThreadAccessible.Access(); + + AutoYieldJSThreadExecution yield; + + // If we are here it's because a WorkerControlRunnable has been dispatched. + // The runnable could be processed here or it could have already been + // processed by a sync event loop. + // The most important thing this method must do, is to decide if the JS + // execution should continue or not. If the runnable returns an error or if + // the worker status is >= Canceling, we should stop the JS execution. + + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + bool mayContinue = true; + bool scheduledIdleGC = false; + + for (;;) { + // Run all control events now. + auto result = ProcessAllControlRunnables(); + if (result == ProcessAllControlRunnablesResult::Abort) { + mayContinue = false; + } + + bool mayFreeze = data->mFrozen; + + { + MutexAutoLock lock(mMutex); + + if (mayFreeze) { + mayFreeze = mStatus <= Running; + } + + if (mStatus >= Canceling) { + mayContinue = false; + } + } + + if (!mayContinue || !mayFreeze) { + break; + } + + // Cancel the periodic GC timer here before freezing. The idle GC timer + // will clean everything up once it runs. + if (!scheduledIdleGC) { + SetGCTimerMode(IdleTimer); + scheduledIdleGC = true; + } + + while ((mayContinue = MayContinueRunning())) { + MutexAutoLock lock(mMutex); + if (!mControlQueue.IsEmpty()) { + break; + } + + WaitForWorkerEvents(); + } + } + + if (!mayContinue) { + // We want only uncatchable exceptions here. + NS_ASSERTION(!JS_IsExceptionPending(aCx), + "Should not have an exception set here!"); + return false; + } + + // Make sure the periodic timer gets turned back on here. + SetGCTimerMode(PeriodicTimer); + + return true; +} + +void WorkerPrivate::CloseInternal() { + AssertIsOnWorkerThread(); + NotifyInternal(Closing); +} + +bool WorkerPrivate::IsOnCurrentThread() { + // May be called on any thread! + + MOZ_ASSERT(mPRThread); + return PR_GetCurrentThread() == mPRThread; +} + +void WorkerPrivate::ScheduleDeletion(WorkerRanOrNot aRanOrNot) { + AssertIsOnWorkerThread(); + { + // mWorkerThreadAccessible's accessor must be destructed before + // the scheduled Runnable gets to run. + auto data = mWorkerThreadAccessible.Access(); + MOZ_ASSERT(data->mChildWorkers.IsEmpty()); + + MOZ_RELEASE_ASSERT(!data->mDeletionScheduled); + data->mDeletionScheduled.Flip(); + } + MOZ_ASSERT(mSyncLoopStack.IsEmpty()); + MOZ_ASSERT(mPostSyncLoopOperations == 0); + + // If Worker is never ran, clear the mPreStartRunnables. To let the resource + // hold by the pre-submmited runnables. + if (WorkerNeverRan == aRanOrNot) { + ClearPreStartRunnables(); + } + +#ifdef DEBUG + if (WorkerRan == aRanOrNot) { + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + // On the worker thread WorkerRunnable will refuse to run if not nested + // on top of a WorkerThreadPrimaryRunnable. + Unused << NS_WARN_IF(NS_HasPendingEvents(currentThread)); + } +#endif + + if (WorkerPrivate* parent = GetParent()) { + RefPtr<WorkerFinishedRunnable> runnable = + new WorkerFinishedRunnable(parent, this); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to dispatch runnable!"); + } + } else { + if (ExtensionAPIAllowed()) { + MOZ_ASSERT(IsServiceWorker()); + RefPtr<Runnable> extWorkerRunnable = + extensions::CreateWorkerDestroyedRunnable(ServiceWorkerID(), + GetBaseURI()); + // Dispatch as a low priority runnable. + if (NS_FAILED( + DispatchToMainThreadForMessaging(extWorkerRunnable.forget()))) { + NS_WARNING( + "Failed to dispatch runnable to notify extensions worker " + "destroyed"); + } + } + + // Note, this uses the lower priority DispatchToMainThreadForMessaging for + // dispatching TopLevelWorkerFinishedRunnable to the main thread so that + // other relevant runnables are guaranteed to run before it. + RefPtr<TopLevelWorkerFinishedRunnable> runnable = + new TopLevelWorkerFinishedRunnable(this); + if (NS_FAILED(DispatchToMainThreadForMessaging(runnable.forget()))) { + NS_WARNING("Failed to dispatch runnable!"); + } + + // NOTE: Calling any WorkerPrivate methods (or accessing member data) after + // this point is unsafe (the TopLevelWorkerFinishedRunnable just dispatched + // may be able to call ClearSelfAndParentEventTargetRef on this + // WorkerPrivate instance and by the time we get here the WorkerPrivate + // instance destructor may have been already called). + } +} + +bool WorkerPrivate::CollectRuntimeStats( + JS::RuntimeStats* aRtStats, bool aAnonymize) MOZ_NO_THREAD_SAFETY_ANALYSIS { + // We don't have a lock to access mJSContext, but it's safe to access on this + // thread. + AssertIsOnWorkerThread(); + NS_ASSERTION(aRtStats, "Null RuntimeStats!"); + // We don't really own it, but it's safe to access on this thread + NS_ASSERTION(mJSContext, "This must never be null!"); + + return JS::CollectRuntimeStats(mJSContext, aRtStats, nullptr, aAnonymize); +} + +void WorkerPrivate::EnableMemoryReporter() { + auto data = mWorkerThreadAccessible.Access(); + MOZ_ASSERT(!data->mMemoryReporter); + + // No need to lock here since the main thread can't race until we've + // successfully registered the reporter. + data->mMemoryReporter = new MemoryReporter(this); + + if (NS_FAILED(RegisterWeakAsyncMemoryReporter(data->mMemoryReporter))) { + NS_WARNING("Failed to register memory reporter!"); + // No need to lock here since a failed registration means our memory + // reporter can't start running. Just clean up. + data->mMemoryReporter = nullptr; + } +} + +void WorkerPrivate::DisableMemoryReporter() { + auto data = mWorkerThreadAccessible.Access(); + + RefPtr<MemoryReporter> memoryReporter; + { + // Mutex protectes MemoryReporter::mWorkerPrivate which is cleared by + // MemoryReporter::Disable() below. + MutexAutoLock lock(mMutex); + + // There is nothing to do here if the memory reporter was never successfully + // registered. + if (!data->mMemoryReporter) { + return; + } + + // We don't need this set any longer. Swap it out so that we can unregister + // below. + data->mMemoryReporter.swap(memoryReporter); + + // Next disable the memory reporter so that the main thread stops trying to + // signal us. + memoryReporter->Disable(); + } + + // Finally unregister the memory reporter. + if (NS_FAILED(UnregisterWeakMemoryReporter(memoryReporter))) { + NS_WARNING("Failed to unregister memory reporter!"); + } +} + +void WorkerPrivate::WaitForWorkerEvents() { + AUTO_PROFILER_LABEL("WorkerPrivate::WaitForWorkerEvents", IDLE); + + AssertIsOnWorkerThread(); + mMutex.AssertCurrentThreadOwns(); + + // Wait for a worker event. + mCondVar.Wait(); +} + +WorkerPrivate::ProcessAllControlRunnablesResult +WorkerPrivate::ProcessAllControlRunnablesLocked() { + AssertIsOnWorkerThread(); + mMutex.AssertCurrentThreadOwns(); + + AutoYieldJSThreadExecution yield; + + auto result = ProcessAllControlRunnablesResult::Nothing; + + for (;;) { + WorkerControlRunnable* event; + if (!mControlQueue.Pop(event)) { + break; + } + + MutexAutoUnlock unlock(mMutex); + + { + MOZ_ASSERT(event); + AUTO_PROFILE_FOLLOWING_RUNNABLE(event); + if (NS_FAILED(static_cast<nsIRunnable*>(event)->Run())) { + result = ProcessAllControlRunnablesResult::Abort; + } + } + + if (result == ProcessAllControlRunnablesResult::Nothing) { + // We ran at least one thing. + result = ProcessAllControlRunnablesResult::MayContinue; + } + event->Release(); + } + + return result; +} + +void WorkerPrivate::ShutdownModuleLoader() { + AssertIsOnWorkerThread(); + + WorkerGlobalScope* globalScope = GlobalScope(); + if (globalScope) { + if (globalScope->GetModuleLoader(nullptr)) { + globalScope->GetModuleLoader(nullptr)->Shutdown(); + } + } + WorkerDebuggerGlobalScope* debugGlobalScope = DebuggerGlobalScope(); + if (debugGlobalScope) { + if (debugGlobalScope->GetModuleLoader(nullptr)) { + debugGlobalScope->GetModuleLoader(nullptr)->Shutdown(); + } + } +} + +void WorkerPrivate::ClearPreStartRunnables() { + nsTArray<RefPtr<WorkerRunnable>> prestart; + { + MutexAutoLock lock(mMutex); + mPreStartRunnables.SwapElements(prestart); + } + for (uint32_t count = prestart.Length(), index = 0; index < count; index++) { + LOG(WorkerLog(), ("WorkerPrivate::ClearPreStartRunnable [%p]", this)); + RefPtr<WorkerRunnable> runnable = std::move(prestart[index]); + runnable->Cancel(); + } +} + +void WorkerPrivate::ClearDebuggerEventQueue() { + while (!mDebuggerQueue.IsEmpty()) { + WorkerRunnable* runnable = nullptr; + mDebuggerQueue.Pop(runnable); + // It should be ok to simply release the runnable, without running it. + runnable->Release(); + } +} + +bool WorkerPrivate::FreezeInternal() { + auto data = mWorkerThreadAccessible.Access(); + NS_ASSERTION(!data->mFrozen, "Already frozen!"); + + AutoYieldJSThreadExecution yield; + + // The worker can freeze even if it failed to run (and doesn't have a global). + if (data->mScope) { + data->mScope->MutableClientSourceRef().Freeze(); + } + + data->mFrozen = true; + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->Freeze(nullptr); + } + + return true; +} + +bool WorkerPrivate::ThawInternal() { + auto data = mWorkerThreadAccessible.Access(); + NS_ASSERTION(data->mFrozen, "Not yet frozen!"); + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->Thaw(nullptr); + } + + data->mFrozen = false; + + // The worker can thaw even if it failed to run (and doesn't have a global). + if (data->mScope) { + data->mScope->MutableClientSourceRef().Thaw(); + } + + return true; +} + +void WorkerPrivate::PropagateStorageAccessPermissionGrantedInternal() { + auto data = mWorkerThreadAccessible.Access(); + + mLoadInfo.mUseRegularPrincipal = true; + mLoadInfo.mUsingStorageAccess = true; + + WorkerGlobalScope* globalScope = GlobalScope(); + if (globalScope) { + globalScope->StorageAccessPermissionGranted(); + } + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->PropagateStorageAccessPermissionGranted(); + } +} + +void WorkerPrivate::TraverseTimeouts(nsCycleCollectionTraversalCallback& cb) { + auto data = mWorkerThreadAccessible.Access(); + for (uint32_t i = 0; i < data->mTimeouts.Length(); ++i) { + // TODO(erahm): No idea what's going on here. + TimeoutInfo* tmp = data->mTimeouts[i].get(); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHandler) + } +} + +void WorkerPrivate::UnlinkTimeouts() { + auto data = mWorkerThreadAccessible.Access(); + data->mTimeouts.Clear(); +} + +bool WorkerPrivate::AddChildWorker(WorkerPrivate& aChildWorker) { + auto data = mWorkerThreadAccessible.Access(); + +#ifdef DEBUG + { + WorkerStatus currentStatus; + { + MutexAutoLock lock(mMutex); + currentStatus = mStatus; + } + + MOZ_ASSERT(currentStatus == Running); + } +#endif + + NS_ASSERTION(!data->mChildWorkers.Contains(&aChildWorker), + "Already know about this one!"); + data->mChildWorkers.AppendElement(&aChildWorker); + + if (data->mChildWorkers.Length() == 1) { + UpdateCCFlag(CCFlag::IneligibleForChildWorker); + } + + return true; +} + +void WorkerPrivate::RemoveChildWorker(WorkerPrivate& aChildWorker) { + auto data = mWorkerThreadAccessible.Access(); + + NS_ASSERTION(data->mChildWorkers.Contains(&aChildWorker), + "Didn't know about this one!"); + data->mChildWorkers.RemoveElement(&aChildWorker); + + if (data->mChildWorkers.IsEmpty()) { + UpdateCCFlag(CCFlag::EligibleForChildWorker); + } +} + +bool WorkerPrivate::AddWorkerRef(WorkerRef* aWorkerRef, + WorkerStatus aFailStatus) { + MOZ_ASSERT(aWorkerRef); + auto data = mWorkerThreadAccessible.Access(); + + { + MutexAutoLock lock(mMutex); + + LOG(WorkerLog(), + ("WorkerPrivate::AddWorkerRef [%p] mStatus: %u, aFailStatus: (%u)", + this, static_cast<uint8_t>(mStatus), + static_cast<uint8_t>(aFailStatus))); + + if (mStatus >= aFailStatus) { + return false; + } + + // We shouldn't create strong references to workers before their main loop + // begins running. Strong references must be disposed of on the worker + // thread, so strong references from other threads use a control runnable + // for that purpose. If the worker fails to reach the main loop stage then + // no control runnables get run and it would be impossible to get rid of the + // reference properly. + MOZ_DIAGNOSTIC_ASSERT_IF(aWorkerRef->IsPreventingShutdown(), + mStatus >= WorkerStatus::Running); + } + + MOZ_ASSERT(!data->mWorkerRefs.Contains(aWorkerRef), + "Already know about this one!"); + + if (aWorkerRef->IsPreventingShutdown()) { + data->mNumWorkerRefsPreventingShutdownStart += 1; + if (data->mNumWorkerRefsPreventingShutdownStart == 1) { + UpdateCCFlag(CCFlag::IneligibleForWorkerRef); + } + } + + data->mWorkerRefs.AppendElement(aWorkerRef); + return true; +} + +void WorkerPrivate::RemoveWorkerRef(WorkerRef* aWorkerRef) { + MOZ_ASSERT(aWorkerRef); + LOG(WorkerLog(), + ("WorkerPrivate::RemoveWorkerRef [%p] aWorkerRef: %p", this, aWorkerRef)); + auto data = mWorkerThreadAccessible.Access(); + + MOZ_ASSERT(data->mWorkerRefs.Contains(aWorkerRef), + "Didn't know about this one!"); + data->mWorkerRefs.RemoveElement(aWorkerRef); + + if (aWorkerRef->IsPreventingShutdown()) { + data->mNumWorkerRefsPreventingShutdownStart -= 1; + if (!data->mNumWorkerRefsPreventingShutdownStart) { + UpdateCCFlag(CCFlag::EligibleForWorkerRef); + } + } +} + +void WorkerPrivate::NotifyWorkerRefs(WorkerStatus aStatus) { + auto data = mWorkerThreadAccessible.Access(); + + NS_ASSERTION(aStatus > Closing, "Bad status!"); + + LOG(WorkerLog(), ("WorkerPrivate::NotifyWorkerRefs [%p] aStatus: %u", this, + static_cast<uint8_t>(aStatus))); + + for (auto* workerRef : data->mWorkerRefs.ForwardRange()) { + LOG(WorkerLog(), ("WorkerPrivate::NotifyWorkerRefs [%p] WorkerRefs(%s %p)", + this, workerRef->mName, workerRef)); + workerRef->Notify(); + } + + AutoTArray<CheckedUnsafePtr<WorkerPrivate>, 10> children; + children.AppendElements(data->mChildWorkers); + + for (uint32_t index = 0; index < children.Length(); index++) { + if (!children[index]->Notify(aStatus)) { + NS_WARNING("Failed to notify child worker!"); + } + } +} + +nsresult WorkerPrivate::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + + MutexAutoLock lock(mMutex); + + // If we've already started running shutdown tasks, don't allow registering + // new ones. + if (mShutdownTasksRun) { + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(!mShutdownTasks.Contains(aTask)); + mShutdownTasks.AppendElement(aTask); + return NS_OK; +} + +nsresult WorkerPrivate::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + + MutexAutoLock lock(mMutex); + + // We've already started running shutdown tasks, so can't unregister them + // anymore. + if (mShutdownTasksRun) { + return NS_ERROR_UNEXPECTED; + } + + return mShutdownTasks.RemoveElement(aTask) ? NS_OK : NS_ERROR_UNEXPECTED; +} + +void WorkerPrivate::RunShutdownTasks() { + nsTArray<nsCOMPtr<nsITargetShutdownTask>> shutdownTasks; + + { + MutexAutoLock lock(mMutex); + shutdownTasks = std::move(mShutdownTasks); + mShutdownTasks.Clear(); + mShutdownTasksRun = true; + } + + for (auto& task : shutdownTasks) { + task->TargetShutdown(); + } + mWorkerHybridEventTarget->ForgetWorkerPrivate(this); +} + +void WorkerPrivate::AdjustNonblockingCCBackgroundActorCount(int32_t aCount) { + AssertIsOnWorkerThread(); + auto data = mWorkerThreadAccessible.Access(); + LOGV(("WorkerPrivate::AdjustNonblockingCCBackgroundActors [%p] (%d/%u)", this, + aCount, data->mNonblockingCCBackgroundActorCount)); + +#ifdef DEBUG + if (aCount < 0) { + MOZ_ASSERT(data->mNonblockingCCBackgroundActorCount >= + (uint32_t)abs(aCount)); + } +#endif + + data->mNonblockingCCBackgroundActorCount += aCount; +} + +void WorkerPrivate::UpdateCCFlag(const CCFlag aFlag) { + LOGV(("WorkerPrivate::UpdateCCFlag [%p]", this)); + AssertIsOnWorkerThread(); + + auto data = mWorkerThreadAccessible.Access(); + +#ifdef DEBUG + switch (aFlag) { + case CCFlag::EligibleForWorkerRef: { + MOZ_ASSERT(!data->mNumWorkerRefsPreventingShutdownStart); + break; + } + case CCFlag::IneligibleForWorkerRef: { + MOZ_ASSERT(data->mNumWorkerRefsPreventingShutdownStart); + break; + } + case CCFlag::EligibleForChildWorker: { + MOZ_ASSERT(data->mChildWorkers.IsEmpty()); + break; + } + case CCFlag::IneligibleForChildWorker: { + MOZ_ASSERT(!data->mChildWorkers.IsEmpty()); + break; + } + case CCFlag::EligibleForTimeout: { + MOZ_ASSERT(data->mTimeouts.IsEmpty()); + break; + } + case CCFlag::IneligibleForTimeout: { + MOZ_ASSERT(!data->mTimeouts.IsEmpty()); + break; + } + case CCFlag::CheckBackgroundActors: { + break; + } + } +#endif + + { + MutexAutoLock lock(mMutex); + if (mStatus > Canceling) { + mCCFlagSaysEligible = true; + return; + } + } + auto HasBackgroundActors = [nonblockingActorCount = + data->mNonblockingCCBackgroundActorCount]() { + RefPtr<PBackgroundChild> backgroundChild = + BackgroundChild::GetForCurrentThread(); + MOZ_ASSERT(backgroundChild); + auto totalCount = backgroundChild->AllManagedActorsCount(); + LOGV(("WorkerPrivate::UpdateCCFlag HasBackgroundActors: %s(%u/%u)", + totalCount > nonblockingActorCount ? "true" : "false", totalCount, + nonblockingActorCount)); + + return totalCount > nonblockingActorCount; + }; + + bool eligibleForCC = data->mChildWorkers.IsEmpty() && + data->mTimeouts.IsEmpty() && + !data->mNumWorkerRefsPreventingShutdownStart; + + // Only checking BackgroundActors when no strong WorkerRef, ChildWorker, and + // Timeout since the checking is expensive. + if (eligibleForCC) { + eligibleForCC = !HasBackgroundActors(); + } + + { + MutexAutoLock lock(mMutex); + mCCFlagSaysEligible = eligibleForCC; + } +} + +bool WorkerPrivate::IsEligibleForCC() { + LOGV(("WorkerPrivate::IsEligibleForCC [%p]", this)); + MutexAutoLock lock(mMutex); + if (mStatus > Canceling) { + return true; + } + + bool hasShutdownTasks = !mShutdownTasks.IsEmpty(); + bool hasPendingEvents = false; + if (mThread) { + hasPendingEvents = + NS_SUCCEEDED(mThread->HasPendingEvents(&hasPendingEvents)) && + hasPendingEvents; + } + + LOGV(("mMainThreadEventTarget: %s", + mMainThreadEventTarget->IsEmpty() ? "empty" : "non-empty")); + LOGV(("mMainThreadEventTargetForMessaging: %s", + mMainThreadEventTargetForMessaging->IsEmpty() ? "empty" : "non-empty")); + LOGV(("mMainThreadDebuggerEventTarget: %s", + mMainThreadDebuggeeEventTarget->IsEmpty() ? "empty" : "non-empty")); + LOGV(("mCCFlagSaysEligible: %s", mCCFlagSaysEligible ? "true" : "false")); + LOGV(("hasShutdownTasks: %s", hasShutdownTasks ? "true" : "false")); + LOGV(("hasPendingEvents: %s", hasPendingEvents ? "true" : "false")); + + return mMainThreadEventTarget->IsEmpty() && + mMainThreadEventTargetForMessaging->IsEmpty() && + mMainThreadDebuggeeEventTarget->IsEmpty() && mCCFlagSaysEligible && + !hasShutdownTasks && !hasPendingEvents && mWorkerLoopIsIdle; +} + +void WorkerPrivate::CancelAllTimeouts() { + auto data = mWorkerThreadAccessible.Access(); + + LOG(TimeoutsLog(), ("Worker %p CancelAllTimeouts.\n", this)); + + if (data->mTimerRunning) { + NS_ASSERTION(data->mTimer && data->mTimerRunnable, "Huh?!"); + NS_ASSERTION(!data->mTimeouts.IsEmpty(), "Huh?!"); + + if (NS_FAILED(data->mTimer->Cancel())) { + NS_WARNING("Failed to cancel timer!"); + } + + for (uint32_t index = 0; index < data->mTimeouts.Length(); index++) { + data->mTimeouts[index]->mCanceled = true; + } + + // If mRunningExpiredTimeouts, then the fact that they are all canceled now + // means that the currently executing RunExpiredTimeouts will deal with + // them. Otherwise, we need to clean them up ourselves. + if (!data->mRunningExpiredTimeouts) { + data->mTimeouts.Clear(); + UpdateCCFlag(CCFlag::EligibleForTimeout); + } + + // Set mTimerRunning false even if mRunningExpiredTimeouts is true, so that + // if we get reentered under this same RunExpiredTimeouts call we don't + // assert above that !mTimeouts().IsEmpty(), because that's clearly false + // now. + data->mTimerRunning = false; + } +#ifdef DEBUG + else if (!data->mRunningExpiredTimeouts) { + NS_ASSERTION(data->mTimeouts.IsEmpty(), "Huh?!"); + } +#endif + + data->mTimer = nullptr; + data->mTimerRunnable = nullptr; +} + +already_AddRefed<nsISerialEventTarget> WorkerPrivate::CreateNewSyncLoop( + WorkerStatus aFailStatus) { + AssertIsOnWorkerThread(); + MOZ_ASSERT( + aFailStatus >= Canceling, + "Sync loops can be created when the worker is in Running/Closing state!"); + + LOG(WorkerLog(), ("WorkerPrivate::CreateNewSyncLoop [%p] failstatus: %u", + this, static_cast<uint8_t>(aFailStatus))); + + ThreadEventQueue* queue = nullptr; + { + MutexAutoLock lock(mMutex); + + if (mStatus >= aFailStatus) { + return nullptr; + } + queue = static_cast<ThreadEventQueue*>(mThread->EventQueue()); + } + + nsCOMPtr<nsISerialEventTarget> nestedEventTarget = queue->PushEventQueue(); + MOZ_ASSERT(nestedEventTarget); + + RefPtr<EventTarget> workerEventTarget = + new EventTarget(this, nestedEventTarget); + + { + // Modifications must be protected by mMutex in DEBUG builds, see comment + // about mSyncLoopStack in WorkerPrivate.h. +#ifdef DEBUG + MutexAutoLock lock(mMutex); +#endif + + mSyncLoopStack.AppendElement(new SyncLoopInfo(workerEventTarget)); + } + + return workerEventTarget.forget(); +} + +nsresult WorkerPrivate::RunCurrentSyncLoop() { + AssertIsOnWorkerThread(); + LOG(WorkerLog(), ("WorkerPrivate::RunCurrentSyncLoop [%p]", this)); + RefPtr<WorkerThread> thread; + JSContext* cx = GetJSContext(); + MOZ_ASSERT(cx); + // mThread is set before we enter, and is never changed during + // RunCurrentSyncLoop. + { + MutexAutoLock lock(mMutex); + // Copy to local so we don't trigger mutex analysis lower down + // mThread is set before we enter, and is never changed during + // RunCurrentSyncLoop copy to local so we don't trigger mutex analysis + thread = mThread; + } + + AutoPushEventLoopGlobal eventLoopGlobal(this, cx); + + // This should not change between now and the time we finish running this sync + // loop. + uint32_t currentLoopIndex = mSyncLoopStack.Length() - 1; + + SyncLoopInfo* loopInfo = mSyncLoopStack[currentLoopIndex].get(); + + AutoYieldJSThreadExecution yield; + + MOZ_ASSERT(loopInfo); + MOZ_ASSERT(!loopInfo->mHasRun); + MOZ_ASSERT(!loopInfo->mCompleted); + +#ifdef DEBUG + loopInfo->mHasRun = true; +#endif + + { + while (!loopInfo->mCompleted) { + bool normalRunnablesPending = false; + + // Don't block with the periodic GC timer running. + if (!NS_HasPendingEvents(thread)) { + SetGCTimerMode(IdleTimer); + } + + // Wait for something to do. + { + MutexAutoLock lock(mMutex); + + for (;;) { + while (mControlQueue.IsEmpty() && !normalRunnablesPending && + !(normalRunnablesPending = NS_HasPendingEvents(thread))) { + WaitForWorkerEvents(); + } + + auto result = ProcessAllControlRunnablesLocked(); + if (result != ProcessAllControlRunnablesResult::Nothing) { + // The state of the world may have changed. Recheck it if we need to + // continue. + normalRunnablesPending = + result == ProcessAllControlRunnablesResult::MayContinue && + NS_HasPendingEvents(thread); + + // NB: If we processed a NotifyRunnable, we might have run + // non-control runnables, one of which may have shut down the + // sync loop. + if (loopInfo->mCompleted) { + break; + } + } + + // If we *didn't* run any control runnables, this should be unchanged. + MOZ_ASSERT(!loopInfo->mCompleted); + + if (normalRunnablesPending) { + break; + } + } + } + + if (normalRunnablesPending) { + // Make sure the periodic timer is running before we continue. + SetGCTimerMode(PeriodicTimer); + + MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(thread, false)); + + // Now *might* be a good time to GC. Let the JS engine make the + // decision. + if (GetCurrentEventLoopGlobal()) { + // If GetCurrentEventLoopGlobal() is non-null, our JSContext is in a + // Realm, so it's safe to try to GC. + MOZ_ASSERT(JS::CurrentGlobalOrNull(cx)); + JS_MaybeGC(cx); + } + } + } + } + + // Make sure that the stack didn't change underneath us. + MOZ_ASSERT(mSyncLoopStack[currentLoopIndex].get() == loopInfo); + + return DestroySyncLoop(currentLoopIndex); +} + +nsresult WorkerPrivate::DestroySyncLoop(uint32_t aLoopIndex) { + MOZ_ASSERT(!mSyncLoopStack.IsEmpty()); + MOZ_ASSERT(mSyncLoopStack.Length() - 1 == aLoopIndex); + + LOG(WorkerLog(), + ("WorkerPrivate::DestroySyncLoop [%p] aLoopIndex: %u", this, aLoopIndex)); + + AutoYieldJSThreadExecution yield; + + // We're about to delete the loop, stash its event target and result. + const auto& loopInfo = mSyncLoopStack[aLoopIndex]; + + nsresult result = loopInfo->mResult; + + { + RefPtr<nsIEventTarget> nestedEventTarget( + loopInfo->mEventTarget->GetNestedEventTarget()); + MOZ_ASSERT(nestedEventTarget); + + loopInfo->mEventTarget->Shutdown(); + + { + MutexAutoLock lock(mMutex); + static_cast<ThreadEventQueue*>(mThread->EventQueue()) + ->PopEventQueue(nestedEventTarget); + } + } + + // Are we making a 1 -> 0 transition here? + if (mSyncLoopStack.Length() == 1) { + if ((mPostSyncLoopOperations & eDispatchCancelingRunnable)) { + LOG(WorkerLog(), + ("WorkerPrivate::DestroySyncLoop [%p] Dispatching CancelingRunnables", + this)); + DispatchCancelingRunnable(); + } + + mPostSyncLoopOperations = 0; + } + + { + // Modifications must be protected by mMutex in DEBUG builds, see comment + // about mSyncLoopStack in WorkerPrivate.h. +#ifdef DEBUG + MutexAutoLock lock(mMutex); +#endif + + // This will delete |loopInfo|! + mSyncLoopStack.RemoveElementAt(aLoopIndex); + } + + return result; +} + +void WorkerPrivate::DispatchCancelingRunnable() { + // Here we use a normal runnable to know when the current JS chunk of code + // is finished. We cannot use a WorkerRunnable because they are not + // accepted any more by the worker, and we do not want to use a + // WorkerControlRunnable because they are immediately executed. + + LOG(WorkerLog(), ("WorkerPrivate::DispatchCancelingRunnable [%p]", this)); + RefPtr<CancelingRunnable> r = new CancelingRunnable(); + { + MutexAutoLock lock(mMutex); + mThread->nsThread::Dispatch(r.forget(), NS_DISPATCH_NORMAL); + } + + // At the same time, we want to be sure that we interrupt infinite loops. + // The following runnable starts a timer that cancel the worker, from the + // parent thread, after CANCELING_TIMEOUT millseconds. + LOG(WorkerLog(), ("WorkerPrivate::DispatchCancelingRunnable [%p] Setup a " + "timeout canceling", + this)); + RefPtr<CancelingWithTimeoutOnParentRunnable> rr = + new CancelingWithTimeoutOnParentRunnable(this); + rr->Dispatch(); +} + +void WorkerPrivate::ReportUseCounters() { + AssertIsOnWorkerThread(); + + if (mReportedUseCounters) { + return; + } + mReportedUseCounters = true; + + if (IsChromeWorker()) { + return; + } + + const size_t kind = Kind(); + switch (kind) { + case WorkerKindDedicated: + glean::use_counter::dedicated_workers_destroyed.Add(); + break; + case WorkerKindShared: + glean::use_counter::shared_workers_destroyed.Add(); + break; + case WorkerKindService: + glean::use_counter::service_workers_destroyed.Add(); + break; + default: + MOZ_ASSERT(false, "Unknown worker kind"); + return; + } + + Maybe<nsCString> workerPathForLogging; + const bool dumpCounters = StaticPrefs::dom_use_counters_dump_worker(); + if (dumpCounters) { + nsAutoCString path(Domain()); + path.AppendLiteral("("); + NS_ConvertUTF16toUTF8 script(ScriptURL()); + path.Append(script); + path.AppendPrintf(", 0x%p)", this); + workerPathForLogging.emplace(std::move(path)); + } + + const size_t count = static_cast<size_t>(UseCounterWorker::Count); + + const auto workerKind = Kind(); + for (size_t c = 0; c < count; ++c) { + if (!GetUseCounter(static_cast<UseCounterWorker>(c))) { + continue; + } + const char* metricName = + IncrementWorkerUseCounter(static_cast<UseCounterWorker>(c), workerKind); + if (dumpCounters) { + printf_stderr("USE_COUNTER_WORKER: %s - %s\n", metricName, + workerPathForLogging->get()); + } + } +} + +void WorkerPrivate::StopSyncLoop(nsIEventTarget* aSyncLoopTarget, + nsresult aResult) { + AssertValidSyncLoop(aSyncLoopTarget); + + if (!MaybeStopSyncLoop(aSyncLoopTarget, aResult)) { + // TODO: I wonder if we should really ever crash here given the assert. + MOZ_CRASH("Unknown sync loop!"); + } +} + +bool WorkerPrivate::MaybeStopSyncLoop(nsIEventTarget* aSyncLoopTarget, + nsresult aResult) { + AssertIsOnWorkerThread(); + + for (uint32_t index = mSyncLoopStack.Length(); index > 0; index--) { + const auto& loopInfo = mSyncLoopStack[index - 1]; + MOZ_ASSERT(loopInfo); + MOZ_ASSERT(loopInfo->mEventTarget); + + if (loopInfo->mEventTarget == aSyncLoopTarget) { + // Can't assert |loop->mHasRun| here because dispatch failures can cause + // us to bail out early. + MOZ_ASSERT(!loopInfo->mCompleted); + + loopInfo->mResult = aResult; + loopInfo->mCompleted = true; + + loopInfo->mEventTarget->Disable(); + + return true; + } + + MOZ_ASSERT(!SameCOMIdentity(loopInfo->mEventTarget, aSyncLoopTarget)); + } + + return false; +} + +#ifdef DEBUG +void WorkerPrivate::AssertValidSyncLoop(nsIEventTarget* aSyncLoopTarget) { + MOZ_ASSERT(aSyncLoopTarget); + + EventTarget* workerTarget; + nsresult rv = aSyncLoopTarget->QueryInterface( + kDEBUGWorkerEventTargetIID, reinterpret_cast<void**>(&workerTarget)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(workerTarget); + + bool valid = false; + + { + MutexAutoLock lock(mMutex); + + for (uint32_t index = 0; index < mSyncLoopStack.Length(); index++) { + const auto& loopInfo = mSyncLoopStack[index]; + MOZ_ASSERT(loopInfo); + MOZ_ASSERT(loopInfo->mEventTarget); + + if (loopInfo->mEventTarget == aSyncLoopTarget) { + valid = true; + break; + } + + MOZ_ASSERT(!SameCOMIdentity(loopInfo->mEventTarget, aSyncLoopTarget)); + } + } + + MOZ_ASSERT(valid); +} +#endif + +void WorkerPrivate::PostMessageToParent( + JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv) { + LOG(WorkerLog(), ("WorkerPrivate::PostMessageToParent [%p]", this)); + AssertIsOnWorkerThread(); + MOZ_DIAGNOSTIC_ASSERT(IsDedicatedWorker()); + + JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue()); + + aRv = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransferable, + &transferable); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RefPtr<MessageEventRunnable> runnable = + new MessageEventRunnable(this, WorkerRunnable::ParentThread); + + JS::CloneDataPolicy clonePolicy; + + // Parent and dedicated workers are always part of the same cluster. + clonePolicy.allowIntraClusterClonableSharedObjects(); + + if (IsSharedMemoryAllowed()) { + clonePolicy.allowSharedMemoryObjects(); + } + + runnable->Write(aCx, aMessage, transferable, clonePolicy, aRv); + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (!runnable->Dispatch()) { + aRv = NS_ERROR_FAILURE; + } +} + +void WorkerPrivate::EnterDebuggerEventLoop() { + auto data = mWorkerThreadAccessible.Access(); + + JSContext* cx = GetJSContext(); + MOZ_ASSERT(cx); + + AutoPushEventLoopGlobal eventLoopGlobal(this, cx); + AutoYieldJSThreadExecution yield; + + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + + uint32_t currentEventLoopLevel = ++data->mDebuggerEventLoopLevel; + + while (currentEventLoopLevel <= data->mDebuggerEventLoopLevel) { + bool debuggerRunnablesPending = false; + + { + MutexAutoLock lock(mMutex); + + debuggerRunnablesPending = !mDebuggerQueue.IsEmpty(); + } + + // Don't block with the periodic GC timer running. + if (!debuggerRunnablesPending) { + SetGCTimerMode(IdleTimer); + } + + // Wait for something to do + { + MutexAutoLock lock(mMutex); + + std::deque<RefPtr<MicroTaskRunnable>>& debuggerMtQueue = + ccjscx->GetDebuggerMicroTaskQueue(); + while (mControlQueue.IsEmpty() && + !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) && + debuggerMtQueue.empty()) { + WaitForWorkerEvents(); + } + + ProcessAllControlRunnablesLocked(); + + // XXXkhuey should we abort JS on the stack here if we got Abort above? + } + ccjscx->PerformDebuggerMicroTaskCheckpoint(); + if (debuggerRunnablesPending) { + // Start the periodic GC timer if it is not already running. + SetGCTimerMode(PeriodicTimer); + + WorkerRunnable* runnable = nullptr; + + { + MutexAutoLock lock(mMutex); + + mDebuggerQueue.Pop(runnable); + } + + MOZ_ASSERT(runnable); + static_cast<nsIRunnable*>(runnable)->Run(); + runnable->Release(); + + ccjscx->PerformDebuggerMicroTaskCheckpoint(); + + // Now *might* be a good time to GC. Let the JS engine make the decision. + if (GetCurrentEventLoopGlobal()) { + // If GetCurrentEventLoopGlobal() is non-null, our JSContext is in a + // Realm, so it's safe to try to GC. + MOZ_ASSERT(JS::CurrentGlobalOrNull(cx)); + JS_MaybeGC(cx); + } + } + } +} + +void WorkerPrivate::LeaveDebuggerEventLoop() { + auto data = mWorkerThreadAccessible.Access(); + + // TODO: Why lock the mutex if we're accessing data accessible to one thread + // only? + MutexAutoLock lock(mMutex); + + if (data->mDebuggerEventLoopLevel > 0) { + --data->mDebuggerEventLoopLevel; + } +} + +void WorkerPrivate::PostMessageToDebugger(const nsAString& aMessage) { + mDebugger->PostMessageToDebugger(aMessage); +} + +void WorkerPrivate::SetDebuggerImmediate(dom::Function& aHandler, + ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + RefPtr<DebuggerImmediateRunnable> runnable = + new DebuggerImmediateRunnable(this, aHandler); + if (!runnable->Dispatch()) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +void WorkerPrivate::ReportErrorToDebugger(const nsAString& aFilename, + uint32_t aLineno, + const nsAString& aMessage) { + mDebugger->ReportErrorToDebugger(aFilename, aLineno, aMessage); +} + +bool WorkerPrivate::NotifyInternal(WorkerStatus aStatus) { + auto data = mWorkerThreadAccessible.Access(); + + // Yield execution while notifying out-of-module WorkerRefs and cancelling + // runnables. + AutoYieldJSThreadExecution yield; + + NS_ASSERTION(aStatus > Running && aStatus < Dead, "Bad status!"); + + RefPtr<EventTarget> eventTarget; + + // Save the old status and set the new status. + { + MutexAutoLock lock(mMutex); + + LOG(WorkerLog(), + ("WorkerPrivate::NotifyInternal [%p] mStatus: %u, aStatus: %u", this, + static_cast<uint8_t>(mStatus), static_cast<uint8_t>(aStatus))); + + if (mStatus >= aStatus) { + return true; + } + + MOZ_ASSERT_IF(aStatus == Killing, + mStatus == Canceling && mParentStatus == Canceling); + + if (aStatus >= Canceling) { + MutexAutoUnlock unlock(mMutex); + if (data->mScope) { + if (aStatus == Canceling) { + data->mScope->NoteTerminating(); + } else { + data->mScope->NoteShuttingDown(); + } + } + } + + mStatus = aStatus; + + // Mark parent status as closing immediately to avoid new events being + // dispatched after we clear the queue below. + if (aStatus == Closing) { + Close(); + } + + // Synchronize the mParentStatus with mStatus, such that event dispatching + // will fail in proper after WorkerPrivate gets into Killing status. + if (aStatus == Killing) { + mParentStatus = Killing; + } + } + + if (aStatus >= Closing) { + CancelAllTimeouts(); + } + + if (aStatus == Closing && GlobalScope()) { + GlobalScope()->SetIsNotEligibleForMessaging(); + } + + // Let all our holders know the new status. + if (aStatus == Canceling) { + NotifyWorkerRefs(aStatus); + } + + // If the worker script never ran, or failed to compile, we don't need to do + // anything else. + WorkerGlobalScope* global = GlobalScope(); + if (!global) { + if (aStatus == Canceling) { + MOZ_ASSERT(!data->mCancelBeforeWorkerScopeConstructed); + data->mCancelBeforeWorkerScopeConstructed.Flip(); + } + return true; + } + + // Don't abort the script now, but we dispatch a runnable to do it when the + // current JS frame is executed. + if (aStatus == Closing) { + if (!mSyncLoopStack.IsEmpty()) { + LOG(WorkerLog(), ("WorkerPrivate::NotifyInternal [%p] request to " + "dispatch canceling runnables...", + this)); + mPostSyncLoopOperations |= eDispatchCancelingRunnable; + } else { + DispatchCancelingRunnable(); + } + return true; + } + + MOZ_ASSERT(aStatus == Canceling || aStatus == Killing); + + LOG(WorkerLog(), ("WorkerPrivate::NotifyInternal [%p] abort script", this)); + + // Always abort the script. + return false; +} + +void WorkerPrivate::ReportError(JSContext* aCx, + JS::ConstUTF8CharsZ aToStringResult, + JSErrorReport* aReport) { + auto data = mWorkerThreadAccessible.Access(); + + if (!MayContinueRunning() || data->mErrorHandlerRecursionCount == 2) { + return; + } + + NS_ASSERTION(data->mErrorHandlerRecursionCount == 0 || + data->mErrorHandlerRecursionCount == 1, + "Bad recursion logic!"); + + UniquePtr<WorkerErrorReport> report = MakeUnique<WorkerErrorReport>(); + if (aReport) { + report->AssignErrorReport(aReport); + } + + JS::ExceptionStack exnStack(aCx); + if (JS_IsExceptionPending(aCx)) { + if (!JS::StealPendingExceptionStack(aCx, &exnStack)) { + JS_ClearPendingException(aCx); + return; + } + + JS::Rooted<JSObject*> stack(aCx), stackGlobal(aCx); + xpc::FindExceptionStackForConsoleReport( + nullptr, exnStack.exception(), exnStack.stack(), &stack, &stackGlobal); + + if (stack) { + JSAutoRealm ar(aCx, stackGlobal); + report->SerializeWorkerStack(aCx, this, stack); + } + } else { + // ReportError is also used for reporting warnings, + // so there won't be a pending exception. + MOZ_ASSERT(aReport && aReport->isWarning()); + } + + if (report->mMessage.IsEmpty() && aToStringResult) { + nsDependentCString toStringResult(aToStringResult.c_str()); + if (!AppendUTF8toUTF16(toStringResult, report->mMessage, + mozilla::fallible)) { + // Try again, with only a 1 KB string. Do this infallibly this time. + // If the user doesn't have 1 KB to spare we're done anyways. + size_t index = std::min<size_t>(1024, toStringResult.Length()); + + // Drop the last code point that may be cropped. + index = RewindToPriorUTF8Codepoint(toStringResult.BeginReading(), index); + + nsDependentCString truncatedToStringResult(aToStringResult.c_str(), + index); + AppendUTF8toUTF16(truncatedToStringResult, report->mMessage); + } + } + + data->mErrorHandlerRecursionCount++; + + // Don't want to run the scope's error handler if this is a recursive error or + // if we ran out of memory. + bool fireAtScope = data->mErrorHandlerRecursionCount == 1 && + report->mErrorNumber != JSMSG_OUT_OF_MEMORY && + JS::CurrentGlobalOrNull(aCx); + + WorkerErrorReport::ReportError(aCx, this, fireAtScope, nullptr, + std::move(report), 0, exnStack.exception()); + + data->mErrorHandlerRecursionCount--; +} + +// static +void WorkerPrivate::ReportErrorToConsole(const char* aMessage) { + nsTArray<nsString> emptyParams; + WorkerPrivate::ReportErrorToConsole(aMessage, emptyParams); +} + +// static +void WorkerPrivate::ReportErrorToConsole(const char* aMessage, + const nsTArray<nsString>& aParams) { + WorkerPrivate* wp = nullptr; + if (!NS_IsMainThread()) { + wp = GetCurrentThreadWorkerPrivate(); + } + + ReportErrorToConsoleRunnable::Report(wp, aMessage, aParams); +} + +int32_t WorkerPrivate::SetTimeout(JSContext* aCx, TimeoutHandler* aHandler, + int32_t aTimeout, bool aIsInterval, + Timeout::Reason aReason, ErrorResult& aRv) { + auto data = mWorkerThreadAccessible.Access(); + MOZ_ASSERT(aHandler); + + // Reasons that doesn't support cancellation will get -1 as their ids. + int32_t timerId = -1; + if (aReason == Timeout::Reason::eTimeoutOrInterval) { + timerId = data->mNextTimeoutId; + data->mNextTimeoutId += 1; + } + + WorkerStatus currentStatus; + { + MutexAutoLock lock(mMutex); + currentStatus = mStatus; + } + + // If the worker is trying to call setTimeout/setInterval and the parent + // thread has initiated the close process then just silently fail. + if (currentStatus >= Closing) { + return timerId; + } + + auto newInfo = MakeUnique<TimeoutInfo>(); + newInfo->mReason = aReason; + newInfo->mOnChromeWorker = mIsChromeWorker; + newInfo->mIsInterval = aIsInterval; + newInfo->mId = timerId; + if (newInfo->mReason == Timeout::Reason::eTimeoutOrInterval || + newInfo->mReason == Timeout::Reason::eIdleCallbackTimeout) { + newInfo->AccumulateNestingLevel(data->mCurrentTimerNestingLevel); + } + + if (MOZ_UNLIKELY(timerId == INT32_MAX)) { + NS_WARNING("Timeout ids overflowed!"); + if (aReason == Timeout::Reason::eTimeoutOrInterval) { + data->mNextTimeoutId = 1; + } + } + + newInfo->mHandler = aHandler; + + // See if any of the optional arguments were passed. + aTimeout = std::max(0, aTimeout); + newInfo->mInterval = TimeDuration::FromMilliseconds(aTimeout); + newInfo->CalculateTargetTime(); + + const auto& insertedInfo = data->mTimeouts.InsertElementSorted( + std::move(newInfo), GetUniquePtrComparator(data->mTimeouts)); + + LOG(TimeoutsLog(), ("Worker %p has new timeout: delay=%d interval=%s\n", this, + aTimeout, aIsInterval ? "yes" : "no")); + + // If the timeout we just made is set to fire next then we need to update the + // timer, unless we're currently running timeouts. + if (insertedInfo == data->mTimeouts.Elements() && + !data->mRunningExpiredTimeouts) { + if (!data->mTimer) { + data->mTimer = NS_NewTimer(GlobalScope()->SerialEventTarget()); + if (!data->mTimer) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return 0; + } + + data->mTimerRunnable = new RunExpiredTimoutsRunnable(this); + } + + if (!data->mTimerRunning) { + UpdateCCFlag(CCFlag::IneligibleForTimeout); + data->mTimerRunning = true; + } + + if (!RescheduleTimeoutTimer(aCx)) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + } + + return timerId; +} + +void WorkerPrivate::ClearTimeout(int32_t aId, Timeout::Reason aReason) { + MOZ_ASSERT(aReason == Timeout::Reason::eTimeoutOrInterval, + "This timeout reason doesn't support cancellation."); + + auto data = mWorkerThreadAccessible.Access(); + + if (!data->mTimeouts.IsEmpty()) { + NS_ASSERTION(data->mTimerRunning, "Huh?!"); + + for (uint32_t index = 0; index < data->mTimeouts.Length(); index++) { + const auto& info = data->mTimeouts[index]; + if (info->mId == aId && info->mReason == aReason) { + info->mCanceled = true; + break; + } + } + } +} + +bool WorkerPrivate::RunExpiredTimeouts(JSContext* aCx) { + auto data = mWorkerThreadAccessible.Access(); + + // We may be called recursively (e.g. close() inside a timeout) or we could + // have been canceled while this event was pending, bail out if there is + // nothing to do. + if (data->mRunningExpiredTimeouts || !data->mTimerRunning) { + return true; + } + + NS_ASSERTION(data->mTimer && data->mTimerRunnable, "Must have a timer!"); + NS_ASSERTION(!data->mTimeouts.IsEmpty(), "Should have some work to do!"); + + bool retval = true; + + auto comparator = GetUniquePtrComparator(data->mTimeouts); + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + + // We want to make sure to run *something*, even if the timer fired a little + // early. Fudge the value of now to at least include the first timeout. + const TimeStamp actual_now = TimeStamp::Now(); + const TimeStamp now = std::max(actual_now, data->mTimeouts[0]->mTargetTime); + + if (now != actual_now) { + LOG(TimeoutsLog(), ("Worker %p fudged timeout by %f ms.\n", this, + (now - actual_now).ToMilliseconds())); +#ifdef DEBUG + double microseconds = (now - actual_now).ToMicroseconds(); + uint32_t allowedEarlyFiringMicroseconds; + data->mTimer->GetAllowedEarlyFiringMicroseconds( + &allowedEarlyFiringMicroseconds); + MOZ_ASSERT(microseconds < allowedEarlyFiringMicroseconds); +#endif + } + + AutoTArray<TimeoutInfo*, 10> expiredTimeouts; + for (uint32_t index = 0; index < data->mTimeouts.Length(); index++) { + TimeoutInfo* info = data->mTimeouts[index].get(); + if (info->mTargetTime > now) { + break; + } + expiredTimeouts.AppendElement(info); + } + + // Guard against recursion. + data->mRunningExpiredTimeouts = true; + + MOZ_DIAGNOSTIC_ASSERT(data->mCurrentTimerNestingLevel == 0); + + // Run expired timeouts. + for (uint32_t index = 0; index < expiredTimeouts.Length(); index++) { + TimeoutInfo*& info = expiredTimeouts[index]; + AutoRestore<uint32_t> nestingLevel(data->mCurrentTimerNestingLevel); + + if (info->mCanceled) { + continue; + } + + // Set current timer nesting level to current running timer handler's + // nesting level + data->mCurrentTimerNestingLevel = info->mNestingLevel; + + LOG(TimeoutsLog(), + ("Worker %p executing timeout with original delay %f ms.\n", this, + info->mInterval.ToMilliseconds())); + + // Always check JS_IsExceptionPending if something fails, and if + // JS_IsExceptionPending returns false (i.e. uncatchable exception) then + // break out of the loop. + + RefPtr<TimeoutHandler> handler(info->mHandler); + + const char* reason; + switch (info->mReason) { + case Timeout::Reason::eTimeoutOrInterval: + if (info->mIsInterval) { + reason = "setInterval handler"; + } else { + reason = "setTimeout handler"; + } + break; + case Timeout::Reason::eDelayedWebTaskTimeout: + reason = "delayedWebTask handler"; + break; + default: + MOZ_ASSERT(info->mReason == Timeout::Reason::eAbortSignalTimeout); + reason = "AbortSignal Timeout"; + } + if (info->mReason == Timeout::Reason::eTimeoutOrInterval || + info->mReason == Timeout::Reason::eDelayedWebTaskTimeout) { + RefPtr<WorkerGlobalScope> scope(this->GlobalScope()); + CallbackDebuggerNotificationGuard guard( + scope, info->mIsInterval + ? DebuggerNotificationType::SetIntervalCallback + : DebuggerNotificationType::SetTimeoutCallback); + + if (!handler->Call(reason)) { + retval = false; + break; + } + } else { + MOZ_ASSERT(info->mReason == Timeout::Reason::eAbortSignalTimeout); + MOZ_ALWAYS_TRUE(handler->Call(reason)); + } + + NS_ASSERTION(data->mRunningExpiredTimeouts, "Someone changed this!"); + } + + // No longer possible to be called recursively. + data->mRunningExpiredTimeouts = false; + + // Now remove canceled and expired timeouts from the main list. + // NB: The timeouts present in expiredTimeouts must have the same order + // with respect to each other in mTimeouts. That is, mTimeouts is just + // expiredTimeouts with extra elements inserted. There may be unexpired + // timeouts that have been inserted between the expired timeouts if the + // timeout event handler called setTimeout/setInterval. + for (uint32_t index = 0, expiredTimeoutIndex = 0, + expiredTimeoutLength = expiredTimeouts.Length(); + index < data->mTimeouts.Length();) { + const auto& info = data->mTimeouts[index]; + if ((expiredTimeoutIndex < expiredTimeoutLength && + info == expiredTimeouts[expiredTimeoutIndex] && + ++expiredTimeoutIndex) || + info->mCanceled) { + if (info->mIsInterval && !info->mCanceled) { + // Reschedule intervals. + // Reschedule a timeout, if needed, increase the nesting level. + info->AccumulateNestingLevel(info->mNestingLevel); + info->CalculateTargetTime(); + // Don't resort the list here, we'll do that at the end. + ++index; + } else { + data->mTimeouts.RemoveElement(info); + } + } else { + // If info did not match the current entry in expiredTimeouts, it + // shouldn't be there at all. + NS_ASSERTION(!expiredTimeouts.Contains(info), + "Our timeouts are out of order!"); + ++index; + } + } + + data->mTimeouts.Sort(comparator); + + // Either signal the parent that we're no longer using timeouts or reschedule + // the timer. + if (data->mTimeouts.IsEmpty()) { + UpdateCCFlag(CCFlag::EligibleForTimeout); + data->mTimerRunning = false; + } else if (retval && !RescheduleTimeoutTimer(aCx)) { + retval = false; + } + + return retval; +} + +bool WorkerPrivate::RescheduleTimeoutTimer(JSContext* aCx) { + auto data = mWorkerThreadAccessible.Access(); + MOZ_ASSERT(!data->mRunningExpiredTimeouts); + NS_ASSERTION(!data->mTimeouts.IsEmpty(), "Should have some timeouts!"); + NS_ASSERTION(data->mTimer && data->mTimerRunnable, "Should have a timer!"); + + // NB: This is important! The timer may have already fired, e.g. if a timeout + // callback itself calls setTimeout for a short duration and then takes longer + // than that to finish executing. If that has happened, it's very important + // that we don't execute the event that is now pending in our event queue, or + // our code in RunExpiredTimeouts to "fudge" the timeout value will unleash an + // early timeout when we execute the event we're about to queue. + data->mTimer->Cancel(); + + double delta = + (data->mTimeouts[0]->mTargetTime - TimeStamp::Now()).ToMilliseconds(); + uint32_t delay = delta > 0 ? static_cast<uint32_t>(std::ceil( + std::min(delta, double(UINT32_MAX)))) + : 0; + + LOG(TimeoutsLog(), + ("Worker %p scheduled timer for %d ms, %zu pending timeouts\n", this, + delay, data->mTimeouts.Length())); + + nsresult rv = data->mTimer->InitWithCallback(data->mTimerRunnable, delay, + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + JS_ReportErrorASCII(aCx, "Failed to start timer!"); + return false; + } + + return true; +} + +void WorkerPrivate::StartCancelingTimer() { + AssertIsOnParentThread(); + + // return if mCancelingTimer has already existed. + if (mCancelingTimer) { + return; + } + + auto errorCleanup = MakeScopeExit([&] { mCancelingTimer = nullptr; }); + + if (WorkerPrivate* parent = GetParent()) { + mCancelingTimer = NS_NewTimer(parent->ControlEventTarget()); + } else { + mCancelingTimer = NS_NewTimer(); + } + + if (NS_WARN_IF(!mCancelingTimer)) { + return; + } + + // This is not needed if we are already in an advanced shutdown state. + { + MutexAutoLock lock(mMutex); + if (ParentStatus() >= Canceling) { + return; + } + } + + uint32_t cancelingTimeoutMillis = + StaticPrefs::dom_worker_canceling_timeoutMilliseconds(); + + RefPtr<CancelingTimerCallback> callback = new CancelingTimerCallback(this); + nsresult rv = mCancelingTimer->InitWithCallback( + callback, cancelingTimeoutMillis, nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + errorCleanup.release(); +} + +void WorkerPrivate::UpdateContextOptionsInternal( + JSContext* aCx, const JS::ContextOptions& aContextOptions) { + auto data = mWorkerThreadAccessible.Access(); + + JS::ContextOptionsRef(aCx) = aContextOptions; + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->UpdateContextOptions(aContextOptions); + } +} + +void WorkerPrivate::UpdateLanguagesInternal( + const nsTArray<nsString>& aLanguages) { + WorkerGlobalScope* globalScope = GlobalScope(); + RefPtr<WorkerNavigator> nav = globalScope->GetExistingNavigator(); + if (nav) { + nav->SetLanguages(aLanguages); + } + + auto data = mWorkerThreadAccessible.Access(); + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->UpdateLanguages(aLanguages); + } + + RefPtr<Event> event = NS_NewDOMEvent(globalScope, nullptr, nullptr); + + event->InitEvent(u"languagechange"_ns, false, false); + event->SetTrusted(true); + + globalScope->DispatchEvent(*event); +} + +void WorkerPrivate::UpdateJSWorkerMemoryParameterInternal( + JSContext* aCx, JSGCParamKey aKey, Maybe<uint32_t> aValue) { + auto data = mWorkerThreadAccessible.Access(); + + if (aValue) { + JS_SetGCParameter(aCx, aKey, *aValue); + } else { + JS_ResetGCParameter(aCx, aKey); + } + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->UpdateJSWorkerMemoryParameter(aKey, aValue); + } +} + +#ifdef JS_GC_ZEAL +void WorkerPrivate::UpdateGCZealInternal(JSContext* aCx, uint8_t aGCZeal, + uint32_t aFrequency) { + auto data = mWorkerThreadAccessible.Access(); + + JS_SetGCZeal(aCx, aGCZeal, aFrequency); + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->UpdateGCZeal(aGCZeal, aFrequency); + } +} +#endif + +void WorkerPrivate::SetLowMemoryStateInternal(JSContext* aCx, bool aState) { + auto data = mWorkerThreadAccessible.Access(); + + JS::SetLowMemoryState(aCx, aState); + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->SetLowMemoryState(aState); + } +} + +void WorkerPrivate::SetCCCollectedAnything(bool collectedAnything) { + mWorkerThreadAccessible.Access()->mCCCollectedAnything = collectedAnything; +} + +bool WorkerPrivate::isLastCCCollectedAnything() { + return mWorkerThreadAccessible.Access()->mCCCollectedAnything; +} + +void WorkerPrivate::GarbageCollectInternal(JSContext* aCx, bool aShrinking, + bool aCollectChildren) { + // Perform GC followed by CC (the CC is triggered by + // WorkerJSRuntime::CustomGCCallback at the end of the collection). + + auto data = mWorkerThreadAccessible.Access(); + + if (!GlobalScope()) { + // We haven't compiled anything yet. Just bail out. + return; + } + + if (aShrinking || aCollectChildren) { + JS::PrepareForFullGC(aCx); + + if (aShrinking && mSyncLoopStack.IsEmpty()) { + JS::NonIncrementalGC(aCx, JS::GCOptions::Shrink, + JS::GCReason::DOM_WORKER); + + // Check whether the CC collected anything and if so GC again. This is + // necessary to collect all garbage. + if (data->mCCCollectedAnything) { + JS::NonIncrementalGC(aCx, JS::GCOptions::Normal, + JS::GCReason::DOM_WORKER); + } + + if (!aCollectChildren) { + LOG(WorkerLog(), ("Worker %p collected idle garbage\n", this)); + } + } else { + JS::NonIncrementalGC(aCx, JS::GCOptions::Normal, + JS::GCReason::DOM_WORKER); + LOG(WorkerLog(), ("Worker %p collected garbage\n", this)); + } + } else { + JS_MaybeGC(aCx); + LOG(WorkerLog(), ("Worker %p collected periodic garbage\n", this)); + } + + if (aCollectChildren) { + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->GarbageCollect(aShrinking); + } + } +} + +void WorkerPrivate::CycleCollectInternal(bool aCollectChildren) { + auto data = mWorkerThreadAccessible.Access(); + + nsCycleCollector_collect(CCReason::WORKER, nullptr); + + if (aCollectChildren) { + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->CycleCollect(); + } + } +} + +void WorkerPrivate::MemoryPressureInternal() { + auto data = mWorkerThreadAccessible.Access(); + + if (data->mScope) { + RefPtr<Console> console = data->mScope->GetConsoleIfExists(); + if (console) { + console->ClearStorage(); + } + + RefPtr<Performance> performance = data->mScope->GetPerformanceIfExists(); + if (performance) { + performance->MemoryPressure(); + } + + data->mScope->RemoveReportRecords(); + } + + if (data->mDebuggerScope) { + RefPtr<Console> console = data->mDebuggerScope->GetConsoleIfExists(); + if (console) { + console->ClearStorage(); + } + } + + for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { + data->mChildWorkers[index]->MemoryPressure(); + } +} + +void WorkerPrivate::SetThread(WorkerThread* aThread) { + if (aThread) { +#ifdef DEBUG + { + bool isOnCurrentThread; + MOZ_ASSERT(NS_SUCCEEDED(aThread->IsOnCurrentThread(&isOnCurrentThread))); + MOZ_ASSERT(!isOnCurrentThread); + } +#endif + + MOZ_ASSERT(!mPRThread); + mPRThread = PRThreadFromThread(aThread); + MOZ_ASSERT(mPRThread); + + mWorkerThreadAccessible.Transfer(mPRThread); + } else { + MOZ_ASSERT(mPRThread); + } +} + +void WorkerPrivate::SetWorkerPrivateInWorkerThread( + WorkerThread* const aThread) { + LOG(WorkerLog(), + ("WorkerPrivate::SetWorkerPrivateInWorkerThread [%p]", this)); + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(!mThread); + MOZ_ASSERT(mStatus == Pending); + + mThread = aThread; + mThread->SetWorker(WorkerThreadFriendKey{}, this); + + if (!mPreStartRunnables.IsEmpty()) { + for (uint32_t index = 0; index < mPreStartRunnables.Length(); index++) { + MOZ_ALWAYS_SUCCEEDS(mThread->DispatchAnyThread( + WorkerThreadFriendKey{}, mPreStartRunnables[index].forget())); + } + mPreStartRunnables.Clear(); + } +} + +void WorkerPrivate::ResetWorkerPrivateInWorkerThread() { + LOG(WorkerLog(), + ("WorkerPrivate::ResetWorkerPrivateInWorkerThread [%p]", this)); + RefPtr<WorkerThread> doomedThread; + + // Release the mutex before doomedThread. + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(mThread); + + mThread->SetWorker(WorkerThreadFriendKey{}, nullptr); + mThread.swap(doomedThread); +} + +void WorkerPrivate::BeginCTypesCall() { + AssertIsOnWorkerThread(); + auto data = mWorkerThreadAccessible.Access(); + + // Don't try to GC while we're blocked in a ctypes call. + SetGCTimerMode(NoTimer); + + data->mYieldJSThreadExecution.EmplaceBack(); +} + +void WorkerPrivate::EndCTypesCall() { + AssertIsOnWorkerThread(); + auto data = mWorkerThreadAccessible.Access(); + + data->mYieldJSThreadExecution.RemoveLastElement(); + + // Make sure the periodic timer is running before we start running JS again. + SetGCTimerMode(PeriodicTimer); +} + +void WorkerPrivate::BeginCTypesCallback() { + AssertIsOnWorkerThread(); + + // Make sure the periodic timer is running before we start running JS again. + SetGCTimerMode(PeriodicTimer); + + // Re-requesting execution is not needed since the JSRuntime code calling + // this will do an AutoEntryScript. +} + +void WorkerPrivate::EndCTypesCallback() { + AssertIsOnWorkerThread(); + + // Don't try to GC while we're blocked in a ctypes call. + SetGCTimerMode(NoTimer); +} + +bool WorkerPrivate::ConnectMessagePort(JSContext* aCx, + UniqueMessagePortId& aIdentifier) { + AssertIsOnWorkerThread(); + + WorkerGlobalScope* globalScope = GlobalScope(); + + JS::Rooted<JSObject*> jsGlobal(aCx, globalScope->GetWrapper()); + MOZ_ASSERT(jsGlobal); + + // This UniqueMessagePortId is used to create a new port, still connected + // with the other one, but in the worker thread. + ErrorResult rv; + RefPtr<MessagePort> port = MessagePort::Create(globalScope, aIdentifier, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return false; + } + + GlobalObject globalObject(aCx, jsGlobal); + if (globalObject.Failed()) { + return false; + } + + RootedDictionary<MessageEventInit> init(aCx); + init.mData = JS_GetEmptyStringValue(aCx); + init.mBubbles = false; + init.mCancelable = false; + init.mSource.SetValue().SetAsMessagePort() = port; + if (!init.mPorts.AppendElement(port.forget(), fallible)) { + return false; + } + + RefPtr<MessageEvent> event = + MessageEvent::Constructor(globalObject, u"connect"_ns, init); + + event->SetTrusted(true); + + globalScope->DispatchEvent(*event); + + return true; +} + +WorkerGlobalScope* WorkerPrivate::GetOrCreateGlobalScope(JSContext* aCx) { + auto data = mWorkerThreadAccessible.Access(); + + if (data->mScope) { + return data->mScope; + } + + if (IsSharedWorker()) { + data->mScope = + new SharedWorkerGlobalScope(this, CreateClientSource(), WorkerName()); + } else if (IsServiceWorker()) { + data->mScope = new ServiceWorkerGlobalScope( + this, CreateClientSource(), GetServiceWorkerRegistrationDescriptor()); + } else { + data->mScope = new DedicatedWorkerGlobalScope(this, CreateClientSource(), + WorkerName()); + } + + JS::Rooted<JSObject*> global(aCx); + NS_ENSURE_TRUE(data->mScope->WrapGlobalObject(aCx, &global), nullptr); + + JSAutoRealm ar(aCx, global); + + if (!RegisterBindings(aCx, global)) { + data->mScope = nullptr; + return nullptr; + } + + // Worker has already in "Canceling", let the WorkerGlobalScope start dying. + if (data->mCancelBeforeWorkerScopeConstructed) { + data->mScope->NoteTerminating(); + data->mScope->DisconnectGlobalTeardownObservers(); + } + + JS_FireOnNewGlobalObject(aCx, global); + + return data->mScope; +} + +WorkerDebuggerGlobalScope* WorkerPrivate::CreateDebuggerGlobalScope( + JSContext* aCx) { + auto data = mWorkerThreadAccessible.Access(); + MOZ_ASSERT(!data->mDebuggerScope); + + // The debugger global gets a dummy client, not the "real" client used by the + // debugee worker. + auto clientSource = ClientManager::CreateSource( + GetClientType(), HybridEventTarget(), NullPrincipalInfo()); + + data->mDebuggerScope = + new WorkerDebuggerGlobalScope(this, std::move(clientSource)); + + JS::Rooted<JSObject*> global(aCx); + NS_ENSURE_TRUE(data->mDebuggerScope->WrapGlobalObject(aCx, &global), nullptr); + + JSAutoRealm ar(aCx, global); + + if (!RegisterDebuggerBindings(aCx, global)) { + data->mDebuggerScope = nullptr; + return nullptr; + } + + JS_FireOnNewGlobalObject(aCx, global); + + return data->mDebuggerScope; +} + +bool WorkerPrivate::IsOnWorkerThread() const { + // We can't use mThread because it must be protected by mMutex and sometimes + // this method is called when mMutex is already locked. This method should + // always work. + MOZ_ASSERT(mPRThread, + "AssertIsOnWorkerThread() called before a thread was assigned!"); + + return mPRThread == PR_GetCurrentThread(); +} + +#ifdef DEBUG +void WorkerPrivate::AssertIsOnWorkerThread() const { + MOZ_ASSERT(IsOnWorkerThread()); +} +#endif // DEBUG + +void WorkerPrivate::DumpCrashInformation(nsACString& aString) { + auto data = mWorkerThreadAccessible.Access(); + + aString.Append("IsChromeWorker("); + if (IsChromeWorker()) { + aString.Append(NS_ConvertUTF16toUTF8(ScriptURL())); + } else { + aString.Append("false"); + } + aString.Append(")"); + for (const auto* workerRef : data->mWorkerRefs.NonObservingRange()) { + if (workerRef->IsPreventingShutdown()) { + aString.Append("|"); + aString.Append(workerRef->Name()); + const nsCString status = GET_WORKERREF_DEBUG_STATUS(workerRef); + if (!status.IsEmpty()) { + aString.Append("["); + aString.Append(status); + aString.Append("]"); + } + } + } +} + +PerformanceStorage* WorkerPrivate::GetPerformanceStorage() { + MOZ_ASSERT(mPerformanceStorage); + return mPerformanceStorage; +} + +bool WorkerPrivate::ShouldResistFingerprinting(RFPTarget aTarget) const { + return mLoadInfo.mShouldResistFingerprinting && + nsRFPService::IsRFPEnabledFor( + mLoadInfo.mOriginAttributes.mPrivateBrowsingId > + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID, + aTarget, mLoadInfo.mOverriddenFingerprintingSettings); +} + +void WorkerPrivate::SetRemoteWorkerController(RemoteWorkerChild* aController) { + AssertIsOnMainThread(); + MOZ_ASSERT(aController); + MOZ_ASSERT(!mRemoteWorkerController); + + mRemoteWorkerController = aController; +} + +RemoteWorkerChild* WorkerPrivate::GetRemoteWorkerController() { + AssertIsOnMainThread(); + MOZ_ASSERT(mRemoteWorkerController); + return mRemoteWorkerController; +} + +RefPtr<GenericPromise> WorkerPrivate::SetServiceWorkerSkipWaitingFlag() { + AssertIsOnWorkerThread(); + MOZ_ASSERT(IsServiceWorker()); + + RefPtr<RemoteWorkerChild> rwc = mRemoteWorkerController; + + if (!rwc) { + return GenericPromise::CreateAndReject(NS_ERROR_DOM_ABORT_ERR, __func__); + } + + RefPtr<GenericPromise> promise = + rwc->MaybeSendSetServiceWorkerSkipWaitingFlag(); + + return promise; +} + +const nsAString& WorkerPrivate::Id() { + AssertIsOnMainThread(); + + if (mId.IsEmpty()) { + mId = ComputeWorkerPrivateId(); + } + + MOZ_ASSERT(!mId.IsEmpty()); + + return mId; +} + +bool WorkerPrivate::IsSharedMemoryAllowed() const { + if (StaticPrefs:: + dom_postMessage_sharedArrayBuffer_bypassCOOP_COEP_insecure_enabled()) { + return true; + } + + if (mIsPrivilegedAddonGlobal) { + return true; + } + + return CrossOriginIsolated(); +} + +bool WorkerPrivate::CrossOriginIsolated() const { + if (!StaticPrefs:: + dom_postMessage_sharedArrayBuffer_withCOOP_COEP_AtStartup()) { + return false; + } + + return mAgentClusterOpenerPolicy == + nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP; +} + +nsILoadInfo::CrossOriginEmbedderPolicy WorkerPrivate::GetEmbedderPolicy() + const { + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return nsILoadInfo::EMBEDDER_POLICY_NULL; + } + + return mEmbedderPolicy.valueOr(nsILoadInfo::EMBEDDER_POLICY_NULL); +} + +Result<Ok, nsresult> WorkerPrivate::SetEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mEmbedderPolicy.isNothing()); + + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return Ok(); + } + + // https://html.spec.whatwg.org/multipage/browsers.html#check-a-global-object's-embedder-policy + // If ownerPolicy's value is not compatible with cross-origin isolation or + // policy's value is compatible with cross-origin isolation, then return true. + EnsureOwnerEmbedderPolicy(); + nsILoadInfo::CrossOriginEmbedderPolicy ownerPolicy = + mOwnerEmbedderPolicy.valueOr(nsILoadInfo::EMBEDDER_POLICY_NULL); + if (nsContentSecurityManager::IsCompatibleWithCrossOriginIsolation( + ownerPolicy) && + !nsContentSecurityManager::IsCompatibleWithCrossOriginIsolation( + aPolicy)) { + return Err(NS_ERROR_BLOCKED_BY_POLICY); + } + + mEmbedderPolicy.emplace(aPolicy); + + return Ok(); +} + +void WorkerPrivate::InheritOwnerEmbedderPolicyOrNull(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRequest); + + EnsureOwnerEmbedderPolicy(); + + if (mOwnerEmbedderPolicy.isSome()) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + MOZ_ASSERT(channel); + + nsCOMPtr<nsIURI> scriptURI; + MOZ_ALWAYS_SUCCEEDS(channel->GetURI(getter_AddRefs(scriptURI))); + + bool isLocalScriptURI = false; + MOZ_ALWAYS_SUCCEEDS(NS_URIChainHasFlags( + scriptURI, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, + &isLocalScriptURI)); + + MOZ_RELEASE_ASSERT(isLocalScriptURI); + } + + mEmbedderPolicy.emplace( + mOwnerEmbedderPolicy.valueOr(nsILoadInfo::EMBEDDER_POLICY_NULL)); +} + +bool WorkerPrivate::MatchEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) const { + MOZ_ASSERT(NS_IsMainThread()); + + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return true; + } + + return mEmbedderPolicy.value() == aPolicy; +} + +nsILoadInfo::CrossOriginEmbedderPolicy WorkerPrivate::GetOwnerEmbedderPolicy() + const { + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return nsILoadInfo::EMBEDDER_POLICY_NULL; + } + + return mOwnerEmbedderPolicy.valueOr(nsILoadInfo::EMBEDDER_POLICY_NULL); +} + +void WorkerPrivate::EnsureOwnerEmbedderPolicy() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerEmbedderPolicy.isNothing()); + + if (GetParent()) { + mOwnerEmbedderPolicy.emplace(GetParent()->GetEmbedderPolicy()); + } else if (GetWindow() && GetWindow()->GetWindowContext()) { + mOwnerEmbedderPolicy.emplace( + GetWindow()->GetWindowContext()->GetEmbedderPolicy()); + } +} + +nsIPrincipal* WorkerPrivate::GetEffectiveStoragePrincipal() const { + AssertIsOnWorkerThread(); + + if (mLoadInfo.mUseRegularPrincipal) { + return mLoadInfo.mPrincipal; + } + + return mLoadInfo.mPartitionedPrincipal; +} + +const mozilla::ipc::PrincipalInfo& +WorkerPrivate::GetEffectiveStoragePrincipalInfo() const { + AssertIsOnWorkerThread(); + + if (mLoadInfo.mUseRegularPrincipal) { + return *mLoadInfo.mPrincipalInfo; + } + + return *mLoadInfo.mPartitionedPrincipalInfo; +} + +NS_IMPL_ADDREF(WorkerPrivate::EventTarget) +NS_IMPL_RELEASE(WorkerPrivate::EventTarget) + +NS_INTERFACE_MAP_BEGIN(WorkerPrivate::EventTarget) + NS_INTERFACE_MAP_ENTRY(nsISerialEventTarget) + NS_INTERFACE_MAP_ENTRY(nsIEventTarget) + NS_INTERFACE_MAP_ENTRY(nsISupports) +#ifdef DEBUG + // kDEBUGWorkerEventTargetIID is special in that it does not AddRef its + // result. + if (aIID.Equals(kDEBUGWorkerEventTargetIID)) { + *aInstancePtr = this; + return NS_OK; + } else +#endif +NS_INTERFACE_MAP_END + +NS_IMETHODIMP +WorkerPrivate::EventTarget::DispatchFromScript(nsIRunnable* aRunnable, + uint32_t aFlags) { + nsCOMPtr<nsIRunnable> event(aRunnable); + return Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +WorkerPrivate::EventTarget::Dispatch(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags) { + // May be called on any thread! + nsCOMPtr<nsIRunnable> event(aRunnable); + + // Workers only support asynchronous dispatch for now. + if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<WorkerRunnable> workerRunnable; + + MutexAutoLock lock(mMutex); + + if (mDisabled) { + NS_WARNING( + "A runnable was posted to a worker that is already shutting " + "down!"); + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(mNestedEventTarget); + + if (event) { + workerRunnable = mWorkerPrivate->MaybeWrapAsWorkerRunnable(event.forget()); + } + + nsresult rv = + mWorkerPrivate->Dispatch(workerRunnable.forget(), mNestedEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerPrivate::EventTarget::DelayedDispatch(already_AddRefed<nsIRunnable>, + uint32_t) + +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WorkerPrivate::EventTarget::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WorkerPrivate::EventTarget::UnregisterShutdownTask( + nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WorkerPrivate::EventTarget::IsOnCurrentThread(bool* aIsOnCurrentThread) { + // May be called on any thread! + + MOZ_ASSERT(aIsOnCurrentThread); + + MutexAutoLock lock(mMutex); + + if (mShutdown) { + NS_WARNING( + "A worker's event target was used after the worker has shutdown!"); + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(mNestedEventTarget); + + *aIsOnCurrentThread = mNestedEventTarget->IsOnCurrentThread(); + return NS_OK; +} + +NS_IMETHODIMP_(bool) +WorkerPrivate::EventTarget::IsOnCurrentThreadInfallible() { + // May be called on any thread! + + MutexAutoLock lock(mMutex); + + if (mShutdown) { + NS_WARNING( + "A worker's event target was used after the worker has shutdown!"); + return false; + } + + MOZ_ASSERT(mNestedEventTarget); + + return mNestedEventTarget->IsOnCurrentThread(); +} + +WorkerPrivate::AutoPushEventLoopGlobal::AutoPushEventLoopGlobal( + WorkerPrivate* aWorkerPrivate, JSContext* aCx) + : mWorkerPrivate(aWorkerPrivate) { + auto data = mWorkerPrivate->mWorkerThreadAccessible.Access(); + mOldEventLoopGlobal = std::move(data->mCurrentEventLoopGlobal); + if (JSObject* global = JS::CurrentGlobalOrNull(aCx)) { + data->mCurrentEventLoopGlobal = xpc::NativeGlobal(global); + } +} + +WorkerPrivate::AutoPushEventLoopGlobal::~AutoPushEventLoopGlobal() { + auto data = mWorkerPrivate->mWorkerThreadAccessible.Access(); + data->mCurrentEventLoopGlobal = std::move(mOldEventLoopGlobal); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h new file mode 100644 index 0000000000..a670d00975 --- /dev/null +++ b/dom/workers/WorkerPrivate.h @@ -0,0 +1,1666 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_workerprivate_h__ +#define mozilla_dom_workers_workerprivate_h__ + +#include <bitset> +#include "MainThreadUtils.h" +#include "ScriptLoader.h" +#include "js/ContextOptions.h" +#include "mozilla/Attributes.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CondVar.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/OriginTrials.h" +#include "mozilla/RelativeTimeline.h" +#include "mozilla/Result.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/ThreadBound.h" +#include "mozilla/ThreadSafeWeakPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/UseCounter.h" +#include "mozilla/dom/ClientSource.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/Timeout.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/dom/Worker.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerLoadInfo.h" +#include "mozilla/dom/WorkerStatus.h" +#include "mozilla/dom/workerinternals/JSSettings.h" +#include "mozilla/dom/workerinternals/Queue.h" +#include "mozilla/dom/JSExecutionManager.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIEventTarget.h" +#include "nsILoadInfo.h" +#include "nsRFPService.h" +#include "nsTObserverArray.h" +#include "stdint.h" + +class nsIThreadInternal; + +namespace JS { +struct RuntimeStats; +} + +namespace mozilla { +class ThrottledEventQueue; +namespace dom { + +class RemoteWorkerChild; + +// If you change this, the corresponding list in nsIWorkerDebugger.idl needs +// to be updated too. And histograms enum for worker use counters uses the same +// order of worker kind. Please also update dom/base/usecounters.py. +enum WorkerKind : uint8_t { + WorkerKindDedicated, + WorkerKindShared, + WorkerKindService +}; + +class ClientInfo; +class ClientSource; +class Function; +class JSExecutionManager; +class MessagePort; +class UniqueMessagePortId; +class PerformanceStorage; +class TimeoutHandler; +class WorkerControlRunnable; +class WorkerCSPEventListener; +class WorkerDebugger; +class WorkerDebuggerGlobalScope; +class WorkerErrorReport; +class WorkerEventTarget; +class WorkerGlobalScope; +class WorkerRef; +class WorkerRunnable; +class WorkerDebuggeeRunnable; +class WorkerThread; + +// SharedMutex is a small wrapper around an (internal) reference-counted Mutex +// object. It exists to avoid changing a lot of code to use Mutex* instead of +// Mutex&. +class MOZ_CAPABILITY("mutex") SharedMutex { + using Mutex = mozilla::Mutex; + + class MOZ_CAPABILITY("mutex") RefCountedMutex final : public Mutex { + public: + explicit RefCountedMutex(const char* aName) : Mutex(aName) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCountedMutex) + + private: + ~RefCountedMutex() = default; + }; + + const RefPtr<RefCountedMutex> mMutex; + + public: + explicit SharedMutex(const char* aName) + : mMutex(new RefCountedMutex(aName)) {} + + SharedMutex(const SharedMutex& aOther) = default; + + operator Mutex&() MOZ_RETURN_CAPABILITY(this) { return *mMutex; } + + operator const Mutex&() const MOZ_RETURN_CAPABILITY(this) { return *mMutex; } + + // We need these to make thread-safety analysis work + void Lock() MOZ_CAPABILITY_ACQUIRE() { mMutex->Lock(); } + void Unlock() MOZ_CAPABILITY_RELEASE() { mMutex->Unlock(); } + + // We can assert we own 'this', but we can't assert we hold mMutex + void AssertCurrentThreadOwns() const + MOZ_ASSERT_CAPABILITY(this) MOZ_NO_THREAD_SAFETY_ANALYSIS { + mMutex->AssertCurrentThreadOwns(); + } +}; + +nsString ComputeWorkerPrivateId(); + +class WorkerPrivate final + : public RelativeTimeline, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + public: + // Callback invoked on the parent thread when the worker's cancellation is + // about to be requested. This covers both calls to + // WorkerPrivate::Cancel() by the owner as well as self-initiated cancellation + // due to top-level script evaluation failing or close() being invoked on the + // global scope for Dedicated and Shared workers, but not Service Workers as + // they do not expose a close() method. + // + // ### Parent-Initiated Cancellation + // + // When WorkerPrivate::Cancel is invoked on the parent thread (by the binding + // exposed Worker::Terminate), this callback is invoked synchronously inside + // that call. + // + // ### Worker Self-Cancellation + // + // When a worker initiates self-cancellation, the worker's notification to the + // parent thread is a non-blocking, async mechanism triggered by + // `WorkerPrivate::DispatchCancelingRunnable`. + // + // Self-cancellation races a normally scheduled runnable against a timer that + // is scheduled against the parent. The 2 paths initiated by + // DispatchCancelingRunnable are: + // + // 1. A CancelingRunnable is dispatched at the worker's normal event target to + // wait for the event loop to be clear of runnables. When the + // CancelingRunnable runs it will dispatch a CancelingOnParentRunnable to + // its parent which is a normal, non-control WorkerDebuggeeRunnable to + // ensure that any postMessages to the parent or similar events get a + // chance to be processed prior to cancellation. The timer scheduled in + // the next bullet will not be canceled unless + // + // 2. A CancelingWithTimeoutOnParentRunnable control runnable is dispatched + // to the parent to schedule a timer which will (also) fire on the parent + // thread. This handles the case where the worker does not yield + // control-flow, and so the normal runnable scheduled above does not get to + // run in a timely fashion. Because this is a control runnable, if the + // parent is a worker then the runnable will be processed with urgency. + // However, if the worker is top-level, then the control-like throttled + // WorkerPrivate::mMainThreadEventTarget will end up getting used which is + // nsIRunnablePriority::PRIORITY_MEDIUMHIGH and distinct from the + // mMainThreadDebuggeeEventTarget which most runnables (like postMessage) + // use. + // + // The timer will explicitly use the control event target if the parent is + // a worker and the implicit event target (via `NS_NewTimer()`) otherwise. + // The callback is CancelingTimerCallback which just calls + // WorkerPrivate::Cancel. + using CancellationCallback = std::function<void(bool aEverRan)>; + + // Callback invoked on the parent just prior to dropping the worker thread's + // strong reference that keeps the WorkerPrivate alive while the worker thread + // is running. This does not provide a guarantee that the underlying thread + // has fully shutdown, just that the worker logic has fully shutdown. + // + // ### Details + // + // The last thing the worker thread's WorkerThreadPrimaryRunnable does before + // initiating the shutdown of the underlying thread is call ScheduleDeletion. + // ScheduleDeletion dispatches a runnable to the parent to notify it that the + // worker has completed its work and will never touch the WorkerPrivate again + // and that the strong self-reference can be dropped. + // + // For parents that are themselves workers, this will be done by + // WorkerFinishedRunnable which is a WorkerControlRunnable, ensuring that this + // is processed in a timely fashion. For main-thread parents, + // TopLevelWorkerFinishedRunnable will be used and sent via + // mMainThreadEventTargetForMessaging which is a weird ThrottledEventQueue + // which does not provide any ordering guarantees relative to + // mMainThreadDebuggeeEventTarget, so if you want those, you need to enhance + // things. + using TerminationCallback = std::function<void(void)>; + + struct LocationInfo { + nsCString mHref; + nsCString mProtocol; + nsCString mHost; + nsCString mHostname; + nsCString mPort; + nsCString mPathname; + nsCString mSearch; + nsCString mHash; + nsString mOrigin; + }; + + NS_INLINE_DECL_REFCOUNTING(WorkerPrivate) + + static already_AddRefed<WorkerPrivate> Constructor( + JSContext* aCx, const nsAString& aScriptURL, bool aIsChromeWorker, + WorkerKind aWorkerKind, RequestCredentials aRequestCredentials, + const WorkerType aWorkerType, const nsAString& aWorkerName, + const nsACString& aServiceWorkerScope, WorkerLoadInfo* aLoadInfo, + ErrorResult& aRv, nsString aId = u""_ns, + CancellationCallback&& aCancellationCallback = {}, + TerminationCallback&& aTerminationCallback = {}); + + enum LoadGroupBehavior { InheritLoadGroup, OverrideLoadGroup }; + + static nsresult GetLoadInfo( + JSContext* aCx, nsPIDOMWindowInner* aWindow, WorkerPrivate* aParent, + const nsAString& aScriptURL, const enum WorkerType& aWorkerType, + const RequestCredentials& aCredentials, bool aIsChromeWorker, + LoadGroupBehavior aLoadGroupBehavior, WorkerKind aWorkerKind, + WorkerLoadInfo* aLoadInfo); + + void Traverse(nsCycleCollectionTraversalCallback& aCb); + + void ClearSelfAndParentEventTargetRef() { + AssertIsOnParentThread(); + MOZ_ASSERT(mSelfRef); + + if (mTerminationCallback) { + mTerminationCallback(); + mTerminationCallback = nullptr; + } + + mParentEventTargetRef = nullptr; + mSelfRef = nullptr; + } + + // May be called on any thread... + bool Start(); + + // Called on the parent thread. + bool Notify(WorkerStatus aStatus); + + bool Cancel() { return Notify(Canceling); } + + bool Close() MOZ_REQUIRES(mMutex); + + // The passed principal must be the Worker principal in case of a + // ServiceWorker and the loading principal for any other type. + static void OverrideLoadInfoLoadGroup(WorkerLoadInfo& aLoadInfo, + nsIPrincipal* aPrincipal); + + bool IsDebuggerRegistered() MOZ_NO_THREAD_SAFETY_ANALYSIS { + AssertIsOnMainThread(); + + // No need to lock here since this is only ever modified by the same thread. + return mDebuggerRegistered; // would give a thread-safety warning + } + + bool ExtensionAPIAllowed() { + return ( + StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup() && + mExtensionAPIAllowed); + } + + void SetIsDebuggerRegistered(bool aDebuggerRegistered) { + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(mDebuggerRegistered != aDebuggerRegistered); + mDebuggerRegistered = aDebuggerRegistered; + + mCondVar.Notify(); + } + + void WaitForIsDebuggerRegistered(bool aDebuggerRegistered) { + AssertIsOnParentThread(); + + // Yield so that the main thread won't be blocked. + AutoYieldJSThreadExecution yield; + + MOZ_ASSERT(!NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + + while (mDebuggerRegistered != aDebuggerRegistered) { + mCondVar.Wait(); + } + } + + nsresult SetIsDebuggerReady(bool aReady); + + WorkerDebugger* Debugger() const { + AssertIsOnMainThread(); + + MOZ_ASSERT(mDebugger); + return mDebugger; + } + + const OriginTrials& Trials() const { return mLoadInfo.mTrials; } + + void SetDebugger(WorkerDebugger* aDebugger) { + AssertIsOnMainThread(); + + MOZ_ASSERT(mDebugger != aDebugger); + mDebugger = aDebugger; + } + + JS::UniqueChars AdoptDefaultLocale() { + MOZ_ASSERT(mDefaultLocale, + "the default locale must have been successfully set for anyone " + "to be trying to adopt it"); + return std::move(mDefaultLocale); + } + + /** + * Invoked by WorkerThreadPrimaryRunnable::Run if it already called + * SetWorkerPrivateInWorkerThread but has to bail out on initialization before + * calling DoRunLoop because PBackground failed to initialize or something + * like that. Note that there's currently no point earlier than this that + * failure can be reported. + * + * When this happens, the worker will need to be deleted, plus the call to + * SetWorkerPrivateInWorkerThread will have scheduled all the + * mPreStartRunnables which need to be cleaned up after, as well as any + * scheduled control runnables. We're somewhat punting on debugger runnables + * for now, which may leak, but the intent is to moot this whole scenario via + * shutdown blockers, so we don't want the extra complexity right now. + */ + void RunLoopNeverRan(); + + MOZ_CAN_RUN_SCRIPT + void DoRunLoop(JSContext* aCx); + + void UnrootGlobalScopes(); + + bool InterruptCallback(JSContext* aCx); + + bool IsOnCurrentThread(); + + void CloseInternal(); + + bool FreezeInternal(); + + bool ThawInternal(); + + void PropagateStorageAccessPermissionGrantedInternal(); + + void TraverseTimeouts(nsCycleCollectionTraversalCallback& aCallback); + + void UnlinkTimeouts(); + + bool AddChildWorker(WorkerPrivate& aChildWorker); + + void RemoveChildWorker(WorkerPrivate& aChildWorker); + + void PostMessageToParent(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, + ErrorResult& aRv); + + void PostMessageToParentMessagePort(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void EnterDebuggerEventLoop(); + + void LeaveDebuggerEventLoop(); + + void PostMessageToDebugger(const nsAString& aMessage); + + void SetDebuggerImmediate(Function& aHandler, ErrorResult& aRv); + + void ReportErrorToDebugger(const nsAString& aFilename, uint32_t aLineno, + const nsAString& aMessage); + + bool NotifyInternal(WorkerStatus aStatus); + + void ReportError(JSContext* aCx, JS::ConstUTF8CharsZ aToStringResult, + JSErrorReport* aReport); + + static void ReportErrorToConsole(const char* aMessage); + + static void ReportErrorToConsole(const char* aMessage, + const nsTArray<nsString>& aParams); + + int32_t SetTimeout(JSContext* aCx, TimeoutHandler* aHandler, int32_t aTimeout, + bool aIsInterval, Timeout::Reason aReason, + ErrorResult& aRv); + + void ClearTimeout(int32_t aId, Timeout::Reason aReason); + + MOZ_CAN_RUN_SCRIPT bool RunExpiredTimeouts(JSContext* aCx); + + bool RescheduleTimeoutTimer(JSContext* aCx); + + void UpdateContextOptionsInternal(JSContext* aCx, + const JS::ContextOptions& aContextOptions); + + void UpdateLanguagesInternal(const nsTArray<nsString>& aLanguages); + + void UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, JSGCParamKey key, + Maybe<uint32_t> aValue); + + enum WorkerRanOrNot { WorkerNeverRan = 0, WorkerRan }; + + void ScheduleDeletion(WorkerRanOrNot aRanOrNot); + + bool CollectRuntimeStats(JS::RuntimeStats* aRtStats, bool aAnonymize); + +#ifdef JS_GC_ZEAL + void UpdateGCZealInternal(JSContext* aCx, uint8_t aGCZeal, + uint32_t aFrequency); +#endif + + void SetLowMemoryStateInternal(JSContext* aCx, bool aState); + + void GarbageCollectInternal(JSContext* aCx, bool aShrinking, + bool aCollectChildren); + + void CycleCollectInternal(bool aCollectChildren); + + void OfflineStatusChangeEventInternal(bool aIsOffline); + + void MemoryPressureInternal(); + + typedef MozPromise<uint64_t, nsresult, true> JSMemoryUsagePromise; + RefPtr<JSMemoryUsagePromise> GetJSMemoryUsage(); + + void SetFetchHandlerWasAdded() { + MOZ_ASSERT(IsServiceWorker()); + AssertIsOnWorkerThread(); + mFetchHandlerWasAdded = true; + } + + bool FetchHandlerWasAdded() const { + MOZ_ASSERT(IsServiceWorker()); + AssertIsOnWorkerThread(); + return mFetchHandlerWasAdded; + } + + JSContext* GetJSContext() const MOZ_NO_THREAD_SAFETY_ANALYSIS { + // mJSContext is only modified on the worker thread, so workerthread code + // can safely read it without a lock + AssertIsOnWorkerThread(); + return mJSContext; + } + + WorkerGlobalScope* GlobalScope() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mScope; + } + + WorkerDebuggerGlobalScope* DebuggerGlobalScope() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mDebuggerScope; + } + + // Get the global associated with the current nested event loop. Will return + // null if we're not in a nested event loop or that nested event loop does not + // have an associated global. + nsIGlobalObject* GetCurrentEventLoopGlobal() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mCurrentEventLoopGlobal; + } + + nsICSPEventListener* CSPEventListener() const; + + void SetThread(WorkerThread* aThread); + + void SetWorkerPrivateInWorkerThread(WorkerThread* aThread); + + void ResetWorkerPrivateInWorkerThread(); + + bool IsOnWorkerThread() const; + + void AssertIsOnWorkerThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + // This may block! + void BeginCTypesCall(); + + // This may block! + void EndCTypesCall(); + + void BeginCTypesCallback(); + + void EndCTypesCallback(); + + bool ConnectMessagePort(JSContext* aCx, UniqueMessagePortId& aIdentifier); + + WorkerGlobalScope* GetOrCreateGlobalScope(JSContext* aCx); + + WorkerDebuggerGlobalScope* CreateDebuggerGlobalScope(JSContext* aCx); + + bool RegisterBindings(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + + bool RegisterDebuggerBindings(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + + bool OnLine() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mOnLine; + } + + void StopSyncLoop(nsIEventTarget* aSyncLoopTarget, nsresult aResult); + + bool MaybeStopSyncLoop(nsIEventTarget* aSyncLoopTarget, nsresult aResult); + + void ShutdownModuleLoader(); + + void ClearPreStartRunnables(); + + void ClearDebuggerEventQueue(); + + void OnProcessNextEvent(); + + void AfterProcessNextEvent(); + + void AssertValidSyncLoop(nsIEventTarget* aSyncLoopTarget) +#ifdef DEBUG + ; +#else + { + } +#endif + + void AssertIsNotPotentiallyLastGCCCRunning() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + auto data = mWorkerThreadAccessible.Access(); + MOZ_DIAGNOSTIC_ASSERT(!data->mIsPotentiallyLastGCCCRunning); +#endif + } + + void SetWorkerScriptExecutedSuccessfully() { + AssertIsOnWorkerThread(); + // Should only be called once! + MOZ_ASSERT(!mWorkerScriptExecutedSuccessfully); + mWorkerScriptExecutedSuccessfully = true; + } + + // Only valid after CompileScriptRunnable has finished running! + bool WorkerScriptExecutedSuccessfully() const { + AssertIsOnWorkerThread(); + return mWorkerScriptExecutedSuccessfully; + } + + // Get the event target to use when dispatching to the main thread + // from this Worker thread. This may be the main thread itself or + // a ThrottledEventQueue to the main thread. + nsISerialEventTarget* MainThreadEventTargetForMessaging(); + + nsresult DispatchToMainThreadForMessaging( + nsIRunnable* aRunnable, uint32_t aFlags = NS_DISPATCH_NORMAL); + + nsresult DispatchToMainThreadForMessaging( + already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags = NS_DISPATCH_NORMAL); + + nsISerialEventTarget* MainThreadEventTarget(); + + nsresult DispatchToMainThread(nsIRunnable* aRunnable, + uint32_t aFlags = NS_DISPATCH_NORMAL); + + nsresult DispatchToMainThread(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags = NS_DISPATCH_NORMAL); + + nsresult DispatchDebuggeeToMainThread( + already_AddRefed<WorkerDebuggeeRunnable> aRunnable, + uint32_t aFlags = NS_DISPATCH_NORMAL); + + // Get an event target that will dispatch runnables as control runnables on + // the worker thread. Implement nsICancelableRunnable if you wish to take + // action on cancelation. + nsISerialEventTarget* ControlEventTarget(); + + // Get an event target that will attempt to dispatch a normal WorkerRunnable, + // but if that fails will then fall back to a control runnable. + nsISerialEventTarget* HybridEventTarget(); + + void DumpCrashInformation(nsACString& aString); + + ClientType GetClientType() const; + + bool EnsureCSPEventListener(); + + void EnsurePerformanceStorage(); + + bool GetExecutionGranted() const; + void SetExecutionGranted(bool aGranted); + + void ScheduleTimeSliceExpiration(uint32_t aDelay); + void CancelTimeSliceExpiration(); + + JSExecutionManager* GetExecutionManager() const; + void SetExecutionManager(JSExecutionManager* aManager); + + void ExecutionReady(); + + PerformanceStorage* GetPerformanceStorage(); + + bool IsAcceptingEvents() MOZ_EXCLUDES(mMutex) { + AssertIsOnParentThread(); + + MutexAutoLock lock(mMutex); + return mParentStatus < Canceling; + } + + WorkerStatus ParentStatusProtected() { + AssertIsOnParentThread(); + MutexAutoLock lock(mMutex); + return mParentStatus; + } + + WorkerStatus ParentStatus() const MOZ_REQUIRES(mMutex) { + mMutex.AssertCurrentThreadOwns(); + return mParentStatus; + } + + Worker* ParentEventTargetRef() const { + MOZ_DIAGNOSTIC_ASSERT(mParentEventTargetRef); + return mParentEventTargetRef; + } + + void SetParentEventTargetRef(Worker* aParentEventTargetRef) { + MOZ_DIAGNOSTIC_ASSERT(aParentEventTargetRef); + MOZ_DIAGNOSTIC_ASSERT(!mParentEventTargetRef); + mParentEventTargetRef = aParentEventTargetRef; + } + + // Check whether this worker is a secure context. For use from the parent + // thread only; the canonical "is secure context" boolean is stored on the + // compartment of the worker global. The only reason we don't + // AssertIsOnParentThread() here is so we can assert that this value matches + // the one on the compartment, which has to be done from the worker thread. + bool IsSecureContext() const { return mIsSecureContext; } + + // Check whether we're running in automation. + bool IsInAutomation() const { return mIsInAutomation; } + + bool IsPrivilegedAddonGlobal() const { return mIsPrivilegedAddonGlobal; } + + TimeStamp CreationTimeStamp() const { return mCreationTimeStamp; } + + DOMHighResTimeStamp CreationTime() const { return mCreationTimeHighRes; } + + DOMHighResTimeStamp TimeStampToDOMHighRes(const TimeStamp& aTimeStamp) const { + MOZ_ASSERT(!aTimeStamp.IsNull()); + TimeDuration duration = aTimeStamp - mCreationTimeStamp; + return duration.ToMilliseconds(); + } + + LocationInfo& GetLocationInfo() { return mLocationInfo; } + + void CopyJSSettings(workerinternals::JSSettings& aSettings) { + mozilla::MutexAutoLock lock(mMutex); + aSettings = mJSSettings; + } + + void CopyJSRealmOptions(JS::RealmOptions& aOptions) { + mozilla::MutexAutoLock lock(mMutex); + aOptions = IsChromeWorker() ? mJSSettings.chromeRealmOptions + : mJSSettings.contentRealmOptions; + } + + // The ability to be a chrome worker is orthogonal to the type of + // worker [Dedicated|Shared|Service]. + bool IsChromeWorker() const { return mIsChromeWorker; } + + // TODO: Invariants require that the parent worker out-live any child + // worker, so WorkerPrivate* should be safe in the moment of calling. + // We would like to have stronger type-system annotated/enforced handling. + WorkerPrivate* GetParent() const { return mParent; } + + bool IsFrozen() const { + AssertIsOnParentThread(); + return mParentFrozen; + } + + bool IsParentWindowPaused() const { + AssertIsOnParentThread(); + return mParentWindowPaused; + } + + // When we debug a worker, we want to disconnect the window and the worker + // communication. This happens calling this method. + // Note: this method doesn't suspend the worker! Use Freeze/Thaw instead. + void ParentWindowPaused(); + + void ParentWindowResumed(); + + const nsString& ScriptURL() const { return mScriptURL; } + + const nsString& WorkerName() const { return mWorkerName; } + RequestCredentials WorkerCredentials() const { return mCredentialsMode; } + enum WorkerType WorkerType() const { return mWorkerType; } + + WorkerKind Kind() const { return mWorkerKind; } + + bool IsDedicatedWorker() const { return mWorkerKind == WorkerKindDedicated; } + + bool IsSharedWorker() const { return mWorkerKind == WorkerKindShared; } + + bool IsServiceWorker() const { return mWorkerKind == WorkerKindService; } + + nsContentPolicyType ContentPolicyType() const { + return ContentPolicyType(mWorkerKind); + } + + static nsContentPolicyType ContentPolicyType(WorkerKind aWorkerKind) { + switch (aWorkerKind) { + case WorkerKindDedicated: + return nsIContentPolicy::TYPE_INTERNAL_WORKER; + case WorkerKindShared: + return nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER; + case WorkerKindService: + return nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER; + default: + MOZ_ASSERT_UNREACHABLE("Invalid worker type"); + return nsIContentPolicy::TYPE_INVALID; + } + } + + nsIScriptContext* GetScriptContext() const { + AssertIsOnMainThread(); + return mLoadInfo.mScriptContext; + } + + const nsCString& Domain() const { return mLoadInfo.mDomain; } + + bool IsFromWindow() const { return mLoadInfo.mFromWindow; } + + nsLoadFlags GetLoadFlags() const { return mLoadInfo.mLoadFlags; } + + uint64_t WindowID() const { return mLoadInfo.mWindowID; } + + uint64_t AssociatedBrowsingContextID() const { + return mLoadInfo.mAssociatedBrowsingContextID; + } + + uint64_t ServiceWorkerID() const { return GetServiceWorkerDescriptor().Id(); } + + const nsCString& ServiceWorkerScope() const { + return GetServiceWorkerDescriptor().Scope(); + } + + // This value should never change after the script load completes. Before + // then, it may only be called on the main thread. + nsIURI* GetBaseURI() const { return mLoadInfo.mBaseURI; } + + void SetBaseURI(nsIURI* aBaseURI); + + nsIURI* GetResolvedScriptURI() const { return mLoadInfo.mResolvedScriptURI; } + + const nsString& ServiceWorkerCacheName() const { + MOZ_DIAGNOSTIC_ASSERT(IsServiceWorker()); + AssertIsOnMainThread(); + return mLoadInfo.mServiceWorkerCacheName; + } + + const ServiceWorkerDescriptor& GetServiceWorkerDescriptor() const { + MOZ_DIAGNOSTIC_ASSERT(IsServiceWorker()); + MOZ_DIAGNOSTIC_ASSERT(mLoadInfo.mServiceWorkerDescriptor.isSome()); + return mLoadInfo.mServiceWorkerDescriptor.ref(); + } + + const ServiceWorkerRegistrationDescriptor& + GetServiceWorkerRegistrationDescriptor() const { + MOZ_DIAGNOSTIC_ASSERT(IsServiceWorker()); + MOZ_DIAGNOSTIC_ASSERT( + mLoadInfo.mServiceWorkerRegistrationDescriptor.isSome()); + return mLoadInfo.mServiceWorkerRegistrationDescriptor.ref(); + } + + void UpdateServiceWorkerState(ServiceWorkerState aState) { + MOZ_DIAGNOSTIC_ASSERT(IsServiceWorker()); + MOZ_DIAGNOSTIC_ASSERT(mLoadInfo.mServiceWorkerDescriptor.isSome()); + return mLoadInfo.mServiceWorkerDescriptor.ref().SetState(aState); + } + + const Maybe<ServiceWorkerDescriptor>& GetParentController() const { + return mLoadInfo.mParentController; + } + + const ChannelInfo& GetChannelInfo() const { return mLoadInfo.mChannelInfo; } + + void SetChannelInfo(const ChannelInfo& aChannelInfo) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mLoadInfo.mChannelInfo.IsInitialized()); + MOZ_ASSERT(aChannelInfo.IsInitialized()); + mLoadInfo.mChannelInfo = aChannelInfo; + } + + void InitChannelInfo(nsIChannel* aChannel) { + mLoadInfo.mChannelInfo.InitFromChannel(aChannel); + } + + void InitChannelInfo(const ChannelInfo& aChannelInfo) { + mLoadInfo.mChannelInfo = aChannelInfo; + } + + nsIPrincipal* GetPrincipal() const { return mLoadInfo.mPrincipal; } + + nsIPrincipal* GetLoadingPrincipal() const { + return mLoadInfo.mLoadingPrincipal; + } + + nsIPrincipal* GetPartitionedPrincipal() const { + return mLoadInfo.mPartitionedPrincipal; + } + + nsIPrincipal* GetEffectiveStoragePrincipal() const; + + nsILoadGroup* GetLoadGroup() const { + AssertIsOnMainThread(); + return mLoadInfo.mLoadGroup; + } + + bool UsesSystemPrincipal() const { + return GetPrincipal()->IsSystemPrincipal(); + } + bool UsesAddonOrExpandedAddonPrincipal() const { + return GetPrincipal()->GetIsAddonOrExpandedAddonPrincipal(); + } + + const mozilla::ipc::PrincipalInfo& GetPrincipalInfo() const { + return *mLoadInfo.mPrincipalInfo; + } + + const mozilla::ipc::PrincipalInfo& GetPartitionedPrincipalInfo() const { + return *mLoadInfo.mPartitionedPrincipalInfo; + } + + const mozilla::ipc::PrincipalInfo& GetEffectiveStoragePrincipalInfo() const; + + already_AddRefed<nsIChannel> ForgetWorkerChannel() { + AssertIsOnMainThread(); + return mLoadInfo.mChannel.forget(); + } + + nsPIDOMWindowInner* GetWindow() const { + AssertIsOnMainThread(); + return mLoadInfo.mWindow; + } + + nsPIDOMWindowInner* GetAncestorWindow() const; + + void EvictFromBFCache(); + + nsIContentSecurityPolicy* GetCsp() const { + AssertIsOnMainThread(); + return mLoadInfo.mCSP; + } + + void SetCsp(nsIContentSecurityPolicy* aCSP); + + nsresult SetCSPFromHeaderValues(const nsACString& aCSPHeaderValue, + const nsACString& aCSPReportOnlyHeaderValue); + + void StoreCSPOnClient(); + + const mozilla::ipc::CSPInfo& GetCSPInfo() const { + return *mLoadInfo.mCSPInfo; + } + + void UpdateReferrerInfoFromHeader( + const nsACString& aReferrerPolicyHeaderValue); + + nsIReferrerInfo* GetReferrerInfo() const { return mLoadInfo.mReferrerInfo; } + + ReferrerPolicy GetReferrerPolicy() const { + return mLoadInfo.mReferrerInfo->ReferrerPolicy(); + } + + void SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + mLoadInfo.mReferrerInfo = aReferrerInfo; + } + + bool IsEvalAllowed() const { return mLoadInfo.mEvalAllowed; } + + void SetEvalAllowed(bool aAllowed) { mLoadInfo.mEvalAllowed = aAllowed; } + + bool GetReportEvalCSPViolations() const { + return mLoadInfo.mReportEvalCSPViolations; + } + + void SetReportEvalCSPViolations(bool aReport) { + mLoadInfo.mReportEvalCSPViolations = aReport; + } + + bool IsWasmEvalAllowed() const { return mLoadInfo.mWasmEvalAllowed; } + + void SetWasmEvalAllowed(bool aAllowed) { + mLoadInfo.mWasmEvalAllowed = aAllowed; + } + + bool GetReportWasmEvalCSPViolations() const { + return mLoadInfo.mReportWasmEvalCSPViolations; + } + + void SetReportWasmEvalCSPViolations(bool aReport) { + mLoadInfo.mReportWasmEvalCSPViolations = aReport; + } + + bool XHRParamsAllowed() const { return mLoadInfo.mXHRParamsAllowed; } + + void SetXHRParamsAllowed(bool aAllowed) { + mLoadInfo.mXHRParamsAllowed = aAllowed; + } + + mozilla::StorageAccess StorageAccess() const { + AssertIsOnWorkerThread(); + if (mLoadInfo.mUsingStorageAccess) { + return mozilla::StorageAccess::eAllow; + } + + return mLoadInfo.mStorageAccess; + } + + bool UseRegularPrincipal() const { + AssertIsOnWorkerThread(); + return mLoadInfo.mUseRegularPrincipal; + } + + bool UsingStorageAccess() const { + AssertIsOnWorkerThread(); + return mLoadInfo.mUsingStorageAccess; + } + + nsICookieJarSettings* CookieJarSettings() const { + // Any thread. + MOZ_ASSERT(mLoadInfo.mCookieJarSettings); + return mLoadInfo.mCookieJarSettings; + } + + const net::CookieJarSettingsArgs& CookieJarSettingsArgs() const { + MOZ_ASSERT(mLoadInfo.mCookieJarSettings); + return mLoadInfo.mCookieJarSettingsArgs; + } + + const OriginAttributes& GetOriginAttributes() const { + return mLoadInfo.mOriginAttributes; + } + + // Determine if the SW testing per-window flag is set by devtools + bool ServiceWorkersTestingInWindow() const { + return mLoadInfo.mServiceWorkersTestingInWindow; + } + + // Determine if the worker was created under a third-party context. + bool IsThirdPartyContextToTopWindow() const { + return mLoadInfo.mIsThirdPartyContextToTopWindow; + } + + bool IsWatchedByDevTools() const { return mLoadInfo.mWatchedByDevTools; } + + bool ShouldResistFingerprinting(RFPTarget aTarget) const; + + const Maybe<RFPTarget>& GetOverriddenFingerprintingSettings() const { + return mLoadInfo.mOverriddenFingerprintingSettings; + } + + RemoteWorkerChild* GetRemoteWorkerController(); + + void SetRemoteWorkerController(RemoteWorkerChild* aController); + + RefPtr<GenericPromise> SetServiceWorkerSkipWaitingFlag(); + + // We can assume that an nsPIDOMWindow will be available for Freeze, Thaw + // as these are only used for globals going in and out of the bfcache. + bool Freeze(const nsPIDOMWindowInner* aWindow); + + bool Thaw(const nsPIDOMWindowInner* aWindow); + + void PropagateStorageAccessPermissionGranted(); + + void EnableDebugger(); + + void DisableDebugger(); + + already_AddRefed<WorkerRunnable> MaybeWrapAsWorkerRunnable( + already_AddRefed<nsIRunnable> aRunnable); + + bool ProxyReleaseMainThreadObjects(); + + void SetLowMemoryState(bool aState); + + void GarbageCollect(bool aShrinking); + + void CycleCollect(); + + nsresult SetPrincipalsAndCSPOnMainThread(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal, + nsILoadGroup* aLoadGroup, + nsIContentSecurityPolicy* aCsp); + + nsresult SetPrincipalsAndCSPFromChannel(nsIChannel* aChannel); + + bool FinalChannelPrincipalIsValid(nsIChannel* aChannel); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + bool PrincipalURIMatchesScriptURL(); +#endif + + void UpdateOverridenLoadGroup(nsILoadGroup* aBaseLoadGroup); + + void WorkerScriptLoaded(); + + Document* GetDocument() const; + + void MemoryPressure(); + + void UpdateContextOptions(const JS::ContextOptions& aContextOptions); + + void UpdateLanguages(const nsTArray<nsString>& aLanguages); + + void UpdateJSWorkerMemoryParameter(JSGCParamKey key, Maybe<uint32_t> value); + +#ifdef JS_GC_ZEAL + void UpdateGCZeal(uint8_t aGCZeal, uint32_t aFrequency); +#endif + + void OfflineStatusChangeEvent(bool aIsOffline); + + nsresult Dispatch(already_AddRefed<WorkerRunnable> aRunnable, + nsIEventTarget* aSyncLoopTarget = nullptr); + + nsresult DispatchControlRunnable( + already_AddRefed<WorkerControlRunnable> aWorkerControlRunnable); + + nsresult DispatchDebuggerRunnable( + already_AddRefed<WorkerRunnable> aDebuggerRunnable); + + bool IsOnParentThread() const; + +#ifdef DEBUG + void AssertIsOnParentThread() const; + + void AssertInnerWindowIsCorrect() const; +#else + void AssertIsOnParentThread() const {} + + void AssertInnerWindowIsCorrect() const {} +#endif + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + bool PrincipalIsValid() const; +#endif + + void StartCancelingTimer(); + + const nsAString& Id(); + + const nsID& AgentClusterId() const { return mAgentClusterId; } + + bool IsSharedMemoryAllowed() const; + + // https://whatpr.org/html/4734/structured-data.html#cross-origin-isolated + bool CrossOriginIsolated() const; + + void SetUseCounter(UseCounterWorker aUseCounter) { + MOZ_ASSERT(!mReportedUseCounters); + MOZ_ASSERT(aUseCounter > UseCounterWorker::Unknown); + AssertIsOnWorkerThread(); + mUseCounters[static_cast<size_t>(aUseCounter)] = true; + } + + /** + * COEP Methods + * + * If browser.tabs.remote.useCrossOriginEmbedderPolicy=false, these methods + * will, depending on the return type, return a value that will avoid + * assertion failures or a value that won't block loads. + */ + nsILoadInfo::CrossOriginEmbedderPolicy GetEmbedderPolicy() const; + + // Fails if a policy has already been set or if `aPolicy` violates the owner's + // policy, if an owner exists. + mozilla::Result<Ok, nsresult> SetEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy); + + // `aRequest` is the request loading the worker and must be QI-able to + // `nsIChannel*`. It's used to verify that the worker can indeed inherit its + // owner's COEP (when an owner exists). + // + // TODO: remove `aRequest`; currently, it's required because instances may not + // always know its final, resolved script URL or have access internally to + // `aRequest`. + void InheritOwnerEmbedderPolicyOrNull(nsIRequest* aRequest); + + // Requires a policy to already have been set. + bool MatchEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) const; + + nsILoadInfo::CrossOriginEmbedderPolicy GetOwnerEmbedderPolicy() const; + + void SetCCCollectedAnything(bool collectedAnything); + bool isLastCCCollectedAnything(); + + uint32_t GetCurrentTimerNestingLevel() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mCurrentTimerNestingLevel; + } + + void IncreaseTopLevelWorkerFinishedRunnableCount() { + ++mTopLevelWorkerFinishedRunnableCount; + } + void DecreaseTopLevelWorkerFinishedRunnableCount() { + --mTopLevelWorkerFinishedRunnableCount; + } + void IncreaseWorkerFinishedRunnableCount() { ++mWorkerFinishedRunnableCount; } + void DecreaseWorkerFinishedRunnableCount() { --mWorkerFinishedRunnableCount; } + + void RunShutdownTasks(); + + bool CancelBeforeWorkerScopeConstructed() const { + auto data = mWorkerThreadAccessible.Access(); + return data->mCancelBeforeWorkerScopeConstructed; + } + + enum class CCFlag : uint8_t { + EligibleForWorkerRef, + IneligibleForWorkerRef, + EligibleForChildWorker, + IneligibleForChildWorker, + EligibleForTimeout, + IneligibleForTimeout, + CheckBackgroundActors, + }; + + // When create/release a StrongWorkerRef, child worker, and timeout, this + // method is used to setup if mParentEventTargetRef can get into + // cycle-collection. + // When this method is called, it will also checks if any background actor + // should block the mParentEventTargetRef cycle-collection when there is no + // StrongWorkerRef/ChildWorker/Timeout. + // Worker thread only. + void UpdateCCFlag(const CCFlag); + + // This is used in WorkerPrivate::Traverse() to checking if + // mParentEventTargetRef should get into cycle-collection. + // Parent thread only method. + bool IsEligibleForCC(); + + // A method which adjusts the count of background actors which should not + // block WorkerPrivate::mParentEventTargetRef cycle-collection. + // Worker thread only. + void AdjustNonblockingCCBackgroundActorCount(int32_t aCount); + + private: + WorkerPrivate( + WorkerPrivate* aParent, const nsAString& aScriptURL, bool aIsChromeWorker, + WorkerKind aWorkerKind, RequestCredentials aRequestCredentials, + enum WorkerType aWorkerType, const nsAString& aWorkerName, + const nsACString& aServiceWorkerScope, WorkerLoadInfo& aLoadInfo, + nsString&& aId, const nsID& aAgentClusterId, + const nsILoadInfo::CrossOriginOpenerPolicy aAgentClusterOpenerPolicy, + CancellationCallback&& aCancellationCallback, + TerminationCallback&& aTerminationCallback); + + ~WorkerPrivate(); + + struct AgentClusterIdAndCoop { + nsID mId; + nsILoadInfo::CrossOriginOpenerPolicy mCoop; + }; + + static AgentClusterIdAndCoop ComputeAgentClusterIdAndCoop( + WorkerPrivate* aParent, WorkerKind aWorkerKind, + WorkerLoadInfo* aLoadInfo); + + bool MayContinueRunning() { + AssertIsOnWorkerThread(); + + WorkerStatus status; + { + MutexAutoLock lock(mMutex); + status = mStatus; + } + + if (status < Canceling) { + return true; + } + + return false; + } + + void CancelAllTimeouts(); + + enum class ProcessAllControlRunnablesResult { + // We did not process anything. + Nothing, + // We did process something, states may have changed, but we can keep + // executing script. + MayContinue, + // We did process something, and should not continue executing script. + Abort + }; + + ProcessAllControlRunnablesResult ProcessAllControlRunnables() { + MutexAutoLock lock(mMutex); + return ProcessAllControlRunnablesLocked(); + } + + ProcessAllControlRunnablesResult ProcessAllControlRunnablesLocked() + MOZ_REQUIRES(mMutex); + + void EnableMemoryReporter(); + + void DisableMemoryReporter(); + + void WaitForWorkerEvents() MOZ_REQUIRES(mMutex); + + // If the worker shutdown status is equal or greater then aFailStatus, this + // operation will fail and nullptr will be returned. See WorkerStatus.h for + // more information about the correct value to use. + already_AddRefed<nsISerialEventTarget> CreateNewSyncLoop( + WorkerStatus aFailStatus); + + nsresult RunCurrentSyncLoop(); + + nsresult DestroySyncLoop(uint32_t aLoopIndex); + + void InitializeGCTimers(); + + enum GCTimerMode { PeriodicTimer = 0, IdleTimer, NoTimer }; + + void SetGCTimerMode(GCTimerMode aMode); + + public: + void CancelGCTimers() { SetGCTimerMode(NoTimer); } + + private: + void ShutdownGCTimers(); + + friend class WorkerRef; + + bool AddWorkerRef(WorkerRef* aWorkerRefer, WorkerStatus aFailStatus); + + void RemoveWorkerRef(WorkerRef* aWorkerRef); + + void NotifyWorkerRefs(WorkerStatus aStatus); + + bool HasActiveWorkerRefs() { + auto data = mWorkerThreadAccessible.Access(); + return !(data->mChildWorkers.IsEmpty() && data->mTimeouts.IsEmpty() && + data->mWorkerRefs.IsEmpty()); + } + + friend class WorkerEventTarget; + + nsresult RegisterShutdownTask(nsITargetShutdownTask* aTask); + + nsresult UnregisterShutdownTask(nsITargetShutdownTask* aTask); + + // Internal logic to dispatch a runnable. This is separate from Dispatch() + // to allow runnables to be atomically dispatched in bulk. + nsresult DispatchLockHeld(already_AddRefed<WorkerRunnable> aRunnable, + nsIEventTarget* aSyncLoopTarget, + const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(mMutex); + + // This method dispatches a simple runnable that starts the shutdown procedure + // after a self.close(). This method is called after a ClearMainEventQueue() + // to be sure that the canceling runnable is the only one in the queue. We + // need this async operation to be sure that all the current JS code is + // executed. + void DispatchCancelingRunnable(); + + bool GetUseCounter(UseCounterWorker aUseCounter) { + MOZ_ASSERT(aUseCounter > UseCounterWorker::Unknown); + AssertIsOnWorkerThread(); + return mUseCounters[static_cast<size_t>(aUseCounter)]; + } + + void ReportUseCounters(); + + UniquePtr<ClientSource> CreateClientSource(); + + // This method is called when corresponding script loader processes the COEP + // header for the worker. + // This method should be called only once in the main thread. + // After this method is called the COEP value owner(window/parent worker) is + // cached in mOwnerEmbedderPolicy such that it can be accessed in other + // threads, i.e. WorkerThread. + void EnsureOwnerEmbedderPolicy(); + + class EventTarget; + friend class EventTarget; + friend class AutoSyncLoopHolder; + + struct TimeoutInfo; + + class MemoryReporter; + friend class MemoryReporter; + + friend class mozilla::dom::WorkerThread; + + SharedMutex mMutex; + mozilla::CondVar mCondVar MOZ_GUARDED_BY(mMutex); + + // We cannot make this CheckedUnsafePtr<WorkerPrivate> as this would violate + // our static assert + MOZ_NON_OWNING_REF WorkerPrivate* const mParent; + + const nsString mScriptURL; + + // This is the worker name for shared workers and dedicated workers. + const nsString mWorkerName; + const RequestCredentials mCredentialsMode; + enum WorkerType mWorkerType; + + const WorkerKind mWorkerKind; + + // The worker is owned by its thread, which is represented here. This is set + // in Constructor() and emptied by WorkerFinishedRunnable, and conditionally + // traversed by the cycle collector if no other things preventing shutdown. + // + // There are 4 ways a worker can be terminated: + // 1. GC/CC - When the worker is in idle state (busycount == 0), it allows to + // traverse the 'hidden' mParentEventTargetRef pointer. This is the exposed + // Worker webidl object. Doing this, CC will be able to detect a cycle and + // Unlink is called. In Unlink, Worker calls Cancel(). + // 2. Worker::Cancel() is called - the shutdown procedure starts immediately. + // 3. WorkerScope::Close() is called - Similar to point 2. + // 4. xpcom-shutdown notification - We call Kill(). + RefPtr<Worker> mParentEventTargetRef; + RefPtr<WorkerPrivate> mSelfRef; + + CancellationCallback mCancellationCallback; + + // The termination callback is passed into the constructor on the parent + // thread and invoked by `ClearSelfAndParentEventTargetRef` just before it + // drops its self-ref. + TerminationCallback mTerminationCallback; + + // The lifetime of these objects within LoadInfo is managed explicitly; + // they do not need to be cycle collected. + WorkerLoadInfo mLoadInfo; + LocationInfo mLocationInfo; + + // Protected by mMutex. + workerinternals::JSSettings mJSSettings MOZ_GUARDED_BY(mMutex); + + WorkerDebugger* mDebugger; + + workerinternals::Queue<WorkerControlRunnable*, 4> mControlQueue; + workerinternals::Queue<WorkerRunnable*, 4> mDebuggerQueue; + + // Touched on multiple threads, protected with mMutex. Only modified on the + // worker thread + JSContext* mJSContext MOZ_GUARDED_BY(mMutex); + // mThread is only modified on the Worker thread, before calling DoRunLoop + RefPtr<WorkerThread> mThread MOZ_GUARDED_BY(mMutex); + // mPRThread is only modified on another thread in ScheduleWorker(), and is + // constant for the duration of DoRunLoop. Static mutex analysis doesn't help + // here + PRThread* mPRThread; + + // Accessed from main thread + RefPtr<ThrottledEventQueue> mMainThreadEventTargetForMessaging; + RefPtr<ThrottledEventQueue> mMainThreadEventTarget; + + // Accessed from worker thread and destructing thread + RefPtr<WorkerEventTarget> mWorkerControlEventTarget; + RefPtr<WorkerEventTarget> mWorkerHybridEventTarget; + + // A pauseable queue for WorkerDebuggeeRunnables directed at the main thread. + // See WorkerDebuggeeRunnable for details. + RefPtr<ThrottledEventQueue> mMainThreadDebuggeeEventTarget; + + struct SyncLoopInfo { + explicit SyncLoopInfo(EventTarget* aEventTarget); + + RefPtr<EventTarget> mEventTarget; + nsresult mResult; + bool mCompleted; +#ifdef DEBUG + bool mHasRun; +#endif + }; + + // This is only modified on the worker thread, but in DEBUG builds + // AssertValidSyncLoop function iterates it on other threads. Therefore + // modifications are done with mMutex held *only* in DEBUG builds. + nsTArray<UniquePtr<SyncLoopInfo>> mSyncLoopStack; + + nsCOMPtr<nsITimer> mCancelingTimer; + + // fired on the main thread if the worker script fails to load + nsCOMPtr<nsIRunnable> mLoadFailedRunnable; + + RefPtr<PerformanceStorage> mPerformanceStorage; + + RefPtr<WorkerCSPEventListener> mCSPEventListener; + + // Protected by mMutex. + nsTArray<RefPtr<WorkerRunnable>> mPreStartRunnables MOZ_GUARDED_BY(mMutex); + + // Only touched on the parent thread. Used for both SharedWorker and + // ServiceWorker RemoteWorkers. + RefPtr<RemoteWorkerChild> mRemoteWorkerController; + + JS::UniqueChars mDefaultLocale; // nulled during worker JSContext init + TimeStamp mKillTime; + WorkerStatus mParentStatus MOZ_GUARDED_BY(mMutex); + WorkerStatus mStatus MOZ_GUARDED_BY(mMutex); + + TimeStamp mCreationTimeStamp; + DOMHighResTimeStamp mCreationTimeHighRes; + + // Flags for use counters used directly by this worker. + static_assert(sizeof(UseCounterWorker) <= sizeof(size_t), + "UseCounterWorker is too big"); + static_assert(UseCounterWorker::Count >= static_cast<UseCounterWorker>(0), + "Should be non-negative value and safe to cast to unsigned"); + std::bitset<static_cast<size_t>(UseCounterWorker::Count)> mUseCounters; + bool mReportedUseCounters; + + // This is created while creating the WorkerPrivate, so it's safe to be + // touched on any thread. + const nsID mAgentClusterId; + + // Things touched on worker thread only. + struct WorkerThreadAccessible { + explicit WorkerThreadAccessible(WorkerPrivate* aParent); + + RefPtr<WorkerGlobalScope> mScope; + RefPtr<WorkerDebuggerGlobalScope> mDebuggerScope; + // We cannot make this CheckedUnsafePtr<WorkerPrivate> as this would violate + // our static assert + nsTArray<WorkerPrivate*> mChildWorkers; + nsTObserverArray<WorkerRef*> mWorkerRefs; + nsTArray<UniquePtr<TimeoutInfo>> mTimeouts; + + nsCOMPtr<nsITimer> mTimer; + nsCOMPtr<nsITimerCallback> mTimerRunnable; + + nsCOMPtr<nsITimer> mPeriodicGCTimer; + nsCOMPtr<nsITimer> mIdleGCTimer; + + RefPtr<MemoryReporter> mMemoryReporter; + + // While running a nested event loop, whether a sync loop or a debugger + // event loop we want to keep track of which global is running it, if any, + // so runnables that run off that event loop can get at that information. In + // practice this only matters for various worker debugger runnables running + // against sandboxes, because all other runnables know which globals they + // belong to already. We could also address this by threading the relevant + // global through the chains of runnables involved, but we'd need to thread + // it through some runnables that run on the main thread, and that would + // require some care to make sure things get released on the correct thread, + // which we'd rather avoid. This member is only accessed on the worker + // thread. + nsCOMPtr<nsIGlobalObject> mCurrentEventLoopGlobal; + + // Timer that triggers an interrupt on expiration of the current time slice + nsCOMPtr<nsITimer> mTSTimer; + + // Execution manager used to regulate execution for this worker. + RefPtr<JSExecutionManager> mExecutionManager; + + // Used to relinguish clearance for CTypes Callbacks. + nsTArray<AutoYieldJSThreadExecution> mYieldJSThreadExecution; + + uint32_t mNumWorkerRefsPreventingShutdownStart; + uint32_t mDebuggerEventLoopLevel; + + // This is the count of background actors that binding with IPCWorkerRefs. + // This count would be used in WorkerPrivate::UpdateCCFlag for checking if + // CC should be blocked by background actors. + uint32_t mNonblockingCCBackgroundActorCount; + + uint32_t mErrorHandlerRecursionCount; + int32_t mNextTimeoutId; + + // Tracks the current setTimeout/setInterval nesting level. + // When there isn't a TimeoutHandler on the stack, this will be 0. + // Whenever setTimeout/setInterval are called, a new TimeoutInfo will be + // created with a nesting level one more than the current nesting level, + // saturating at the kClampTimeoutNestingLevel. + // + // When RunExpiredTimeouts is run, it sets this value to the + // TimeoutInfo::mNestingLevel for the duration of + // the WorkerScriptTimeoutHandler::Call which will explicitly trigger a + // microtask checkpoint so that any immediately-resolved promises will + // still see the nesting level. + uint32_t mCurrentTimerNestingLevel; + + bool mFrozen; + bool mTimerRunning; + bool mRunningExpiredTimeouts; + bool mPeriodicGCTimerRunning; + bool mIdleGCTimerRunning; + bool mOnLine; + bool mJSThreadExecutionGranted; + bool mCCCollectedAnything; + FlippedOnce<false> mDeletionScheduled; + FlippedOnce<false> mCancelBeforeWorkerScopeConstructed; + FlippedOnce<false> mPerformedShutdownAfterLastContentTaskExecuted; +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + bool mIsPotentiallyLastGCCCRunning = false; +#endif + }; + ThreadBound<WorkerThreadAccessible> mWorkerThreadAccessible; + + class MOZ_RAII AutoPushEventLoopGlobal { + public: + AutoPushEventLoopGlobal(WorkerPrivate* aWorkerPrivate, JSContext* aCx); + ~AutoPushEventLoopGlobal(); + + private: + // We cannot make this CheckedUnsafePtr<WorkerPrivate> as this would violate + // our static assert + MOZ_NON_OWNING_REF WorkerPrivate* mWorkerPrivate; + nsCOMPtr<nsIGlobalObject> mOldEventLoopGlobal; + }; + friend class AutoPushEventLoopGlobal; + + uint32_t mPostSyncLoopOperations; + + // List of operations to do at the end of the last sync event loop. + enum { + eDispatchCancelingRunnable = 0x02, + }; + + bool mParentWindowPaused; + + bool mWorkerScriptExecutedSuccessfully; + bool mFetchHandlerWasAdded; + bool mMainThreadObjectsForgotten; + bool mIsChromeWorker; + bool mParentFrozen; + + // mIsSecureContext is set once in our constructor; after that it can be read + // from various threads. + // + // It's a bit unfortunate that we have to have an out-of-band boolean for + // this, but we need access to this state from the parent thread, and we can't + // use our global object's secure state there. + const bool mIsSecureContext; + + bool mDebuggerRegistered MOZ_GUARDED_BY(mMutex); + + // During registration, this worker may be marked as not being ready to + // execute debuggee runnables or content. + // + // Protected by mMutex. + bool mDebuggerReady; + nsTArray<RefPtr<WorkerRunnable>> mDelayedDebuggeeRunnables; + + // Whether this worker should have access to the WebExtension API bindings + // (currently only the Extension Background ServiceWorker declared in the + // extension manifest is allowed to access any WebExtension API bindings). + // This default to false, and it is eventually set to true by + // RemoteWorkerChild::ExecWorkerOnMainThread if the needed conditions + // are met. + bool mExtensionAPIAllowed; + + // mIsInAutomation is true when we're running in test automation. + // We expose some extra testing functions in that case. + bool mIsInAutomation; + + nsString mId; + + // This is used to check if it's allowed to share the memory across the agent + // cluster. + const nsILoadInfo::CrossOriginOpenerPolicy mAgentClusterOpenerPolicy; + + // Member variable of this class rather than the worker global scope because + // it's received on the main thread, but the global scope is thread-bound + // to the worker thread, so storing the value in the global scope would + // involve sacrificing the thread-bound-ness or using a WorkerRunnable, and + // there isn't a strong reason to store it on the global scope other than + // better consistency with the COEP spec. + Maybe<nsILoadInfo::CrossOriginEmbedderPolicy> mEmbedderPolicy; + Maybe<nsILoadInfo::CrossOriginEmbedderPolicy> mOwnerEmbedderPolicy; + + /* Privileged add-on flag extracted from the AddonPolicy on the nsIPrincipal + * on the main thread when constructing a top-level worker. The flag is + * propagated to nested workers. The flag is only allowed to take effect in + * extension processes and is forbidden in content scripts in content + * processes. The flag may be read on either the parent/owner thread as well + * as on the worker thread itself. When bug 1443925 is fixed allowing + * nsIPrincipal to be used OMT, it may be possible to remove this flag. */ + bool mIsPrivilegedAddonGlobal; + + Atomic<uint32_t> mTopLevelWorkerFinishedRunnableCount; + Atomic<uint32_t> mWorkerFinishedRunnableCount; + + nsTArray<nsCOMPtr<nsITargetShutdownTask>> mShutdownTasks + MOZ_GUARDED_BY(mMutex); + bool mShutdownTasksRun MOZ_GUARDED_BY(mMutex) = false; + + bool mCCFlagSaysEligible MOZ_GUARDED_BY(mMutex){true}; + + // The flag indicates if the worke is idle for events in the main event loop. + bool mWorkerLoopIsIdle MOZ_GUARDED_BY(mMutex){false}; +}; + +class AutoSyncLoopHolder { + CheckedUnsafePtr<WorkerPrivate> mWorkerPrivate; + nsCOMPtr<nsISerialEventTarget> mTarget; + uint32_t mIndex; + + public: + // See CreateNewSyncLoop() for more information about the correct value to use + // for aFailStatus. + AutoSyncLoopHolder(WorkerPrivate* aWorkerPrivate, WorkerStatus aFailStatus) + : mWorkerPrivate(aWorkerPrivate), + mTarget(aWorkerPrivate->CreateNewSyncLoop(aFailStatus)), + mIndex(aWorkerPrivate->mSyncLoopStack.Length() - 1) { + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + ~AutoSyncLoopHolder() { + if (mWorkerPrivate && mTarget) { + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate->StopSyncLoop(mTarget, NS_ERROR_FAILURE); + mWorkerPrivate->DestroySyncLoop(mIndex); + } + } + + nsresult Run() { + CheckedUnsafePtr<WorkerPrivate> workerPrivate = mWorkerPrivate; + mWorkerPrivate = nullptr; + + workerPrivate->AssertIsOnWorkerThread(); + + return workerPrivate->RunCurrentSyncLoop(); + } + + nsISerialEventTarget* GetSerialEventTarget() const { + // This can be null if CreateNewSyncLoop() fails. + return mTarget; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_workers_workerprivate_h__ */ diff --git a/dom/workers/WorkerRef.cpp b/dom/workers/WorkerRef.cpp new file mode 100644 index 0000000000..9ba9841041 --- /dev/null +++ b/dom/workers/WorkerRef.cpp @@ -0,0 +1,267 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WorkerRef.h" + +#include "nsDebug.h" +#include "WorkerRunnable.h" +#include "WorkerPrivate.h" + +namespace mozilla::dom { + +namespace { + +// This runnable is used to release the StrongWorkerRef on the worker thread +// when a ThreadSafeWorkerRef is released. +class ReleaseRefControlRunnable final : public WorkerControlRunnable { + public: + ReleaseRefControlRunnable(WorkerPrivate* aWorkerPrivate, + already_AddRefed<StrongWorkerRef> aRef) + : WorkerControlRunnable(aWorkerPrivate, "ReleaseRefControlRunnable", + WorkerThread), + mRef(std::move(aRef)) { + MOZ_ASSERT(mRef); + } + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + mRef = nullptr; + return true; + } + + private: + RefPtr<StrongWorkerRef> mRef; +}; + +} // namespace + +// ---------------------------------------------------------------------------- +// WorkerRef + +WorkerRef::WorkerRef(WorkerPrivate* aWorkerPrivate, const char* aName, + bool aIsPreventingShutdown) + : +#ifdef DEBUG + mDebugMutex("WorkerRef"), +#endif + mWorkerPrivate(aWorkerPrivate), + mName(aName), + mIsPreventingShutdown(aIsPreventingShutdown), + mHolding(false) { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aName); + + aWorkerPrivate->AssertIsOnWorkerThread(); +} + +WorkerRef::~WorkerRef() { + NS_ASSERT_OWNINGTHREAD(WorkerRef); + ReleaseWorker(); +} + +void WorkerRef::ReleaseWorker() { + if (mHolding) { + MOZ_ASSERT(mWorkerPrivate); + + if (mIsPreventingShutdown) { + mWorkerPrivate->AssertIsNotPotentiallyLastGCCCRunning(); + } + mWorkerPrivate->RemoveWorkerRef(this); + mWorkerPrivate = nullptr; + + mHolding = false; + } +} + +bool WorkerRef::HoldWorker(WorkerStatus aStatus) { + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(!mHolding); + + if (NS_WARN_IF(!mWorkerPrivate->AddWorkerRef(this, aStatus))) { + return false; + } + + mHolding = true; + return true; +} + +void WorkerRef::Notify() { + NS_ASSERT_OWNINGTHREAD(WorkerRef); + + if (!mCallback) { + return; + } + + MoveOnlyFunction<void()> callback = std::move(mCallback); + MOZ_ASSERT(!mCallback); + + callback(); +} + +// ---------------------------------------------------------------------------- +// WeakWorkerRef + +/* static */ +already_AddRefed<WeakWorkerRef> WeakWorkerRef::Create( + WorkerPrivate* aWorkerPrivate, MoveOnlyFunction<void()>&& aCallback) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<WeakWorkerRef> ref = new WeakWorkerRef(aWorkerPrivate); + if (!ref->HoldWorker(Canceling)) { + return nullptr; + } + + ref->mCallback = std::move(aCallback); + + return ref.forget(); +} + +WeakWorkerRef::WeakWorkerRef(WorkerPrivate* aWorkerPrivate) + : WorkerRef(aWorkerPrivate, "WeakWorkerRef", false) {} + +WeakWorkerRef::~WeakWorkerRef() = default; + +void WeakWorkerRef::Notify() { + MOZ_ASSERT(mHolding); + MOZ_ASSERT(mWorkerPrivate); + + // Notify could drop the last reference to this object. We must keep it alive + // in order to call ReleaseWorker() immediately after. + RefPtr<WeakWorkerRef> kungFuGrip = this; + + WorkerRef::Notify(); + ReleaseWorker(); +} + +WorkerPrivate* WeakWorkerRef::GetPrivate() const { + NS_ASSERT_OWNINGTHREAD(WeakWorkerRef); + return mWorkerPrivate; +} + +WorkerPrivate* WeakWorkerRef::GetUnsafePrivate() const { + return mWorkerPrivate; +} + +// ---------------------------------------------------------------------------- +// StrongWorkerRef + +/* static */ +already_AddRefed<StrongWorkerRef> StrongWorkerRef::Create( + WorkerPrivate* const aWorkerPrivate, const char* const aName, + MoveOnlyFunction<void()>&& aCallback) { + if (RefPtr<StrongWorkerRef> ref = + CreateImpl(aWorkerPrivate, aName, Canceling)) { + ref->mCallback = std::move(aCallback); + return ref.forget(); + } + return nullptr; +} + +/* static */ +already_AddRefed<StrongWorkerRef> StrongWorkerRef::CreateForcibly( + WorkerPrivate* const aWorkerPrivate, const char* const aName) { + return CreateImpl(aWorkerPrivate, aName, Killing); +} + +/* static */ +already_AddRefed<StrongWorkerRef> StrongWorkerRef::CreateImpl( + WorkerPrivate* const aWorkerPrivate, const char* const aName, + WorkerStatus const aFailStatus) { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aName); + + RefPtr<StrongWorkerRef> ref = new StrongWorkerRef(aWorkerPrivate, aName); + if (!ref->HoldWorker(aFailStatus)) { + return nullptr; + } + + return ref.forget(); +} + +StrongWorkerRef::StrongWorkerRef(WorkerPrivate* aWorkerPrivate, + const char* aName) + : WorkerRef(aWorkerPrivate, aName, true) {} + +StrongWorkerRef::~StrongWorkerRef() = default; + +WorkerPrivate* StrongWorkerRef::Private() const { + NS_ASSERT_OWNINGTHREAD(StrongWorkerRef); + return mWorkerPrivate; +} + +// ---------------------------------------------------------------------------- +// ThreadSafeWorkerRef + +ThreadSafeWorkerRef::ThreadSafeWorkerRef(StrongWorkerRef* aRef) : mRef(aRef) { + MOZ_ASSERT(aRef); + aRef->Private()->AssertIsOnWorkerThread(); +} + +ThreadSafeWorkerRef::~ThreadSafeWorkerRef() { + // Let's release the StrongWorkerRef on the correct thread. + if (!mRef->mWorkerPrivate->IsOnWorkerThread()) { + WorkerPrivate* workerPrivate = mRef->mWorkerPrivate; + RefPtr<ReleaseRefControlRunnable> r = + new ReleaseRefControlRunnable(workerPrivate, mRef.forget()); + r->Dispatch(); + return; + } +} + +WorkerPrivate* ThreadSafeWorkerRef::Private() const { + return mRef->mWorkerPrivate; +} + +// ---------------------------------------------------------------------------- +// IPCWorkerRef + +/* static */ +already_AddRefed<IPCWorkerRef> IPCWorkerRef::Create( + WorkerPrivate* aWorkerPrivate, const char* aName, + MoveOnlyFunction<void()>&& aCallback) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<IPCWorkerRef> ref = new IPCWorkerRef(aWorkerPrivate, aName); + if (!ref->HoldWorker(Canceling)) { + return nullptr; + } + ref->SetActorCount(1); + ref->mCallback = std::move(aCallback); + + return ref.forget(); +} + +IPCWorkerRef::IPCWorkerRef(WorkerPrivate* aWorkerPrivate, const char* aName) + : WorkerRef(aWorkerPrivate, aName, false), mActorCount(0) {} + +IPCWorkerRef::~IPCWorkerRef() { + NS_ASSERT_OWNINGTHREAD(IPCWorkerRef); + // explicit type convertion to avoid undefined behavior of uint32_t overflow. + mWorkerPrivate->AdjustNonblockingCCBackgroundActorCount( + (int32_t)-mActorCount); + ReleaseWorker(); +}; + +WorkerPrivate* IPCWorkerRef::Private() const { + NS_ASSERT_OWNINGTHREAD(IPCWorkerRef); + return mWorkerPrivate; +} + +void IPCWorkerRef::SetActorCount(uint32_t aCount) { + NS_ASSERT_OWNINGTHREAD(IPCWorkerRef); + // explicit type convertion to avoid undefined behavior of uint32_t overflow. + mWorkerPrivate->AdjustNonblockingCCBackgroundActorCount((int32_t)aCount - + (int32_t)mActorCount); + mActorCount = aCount; +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerRef.h b/dom/workers/WorkerRef.h new file mode 100644 index 0000000000..e41ef07bfd --- /dev/null +++ b/dom/workers/WorkerRef.h @@ -0,0 +1,276 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerRef_h +#define mozilla_dom_workers_WorkerRef_h + +#include "mozilla/dom/WorkerStatus.h" +#include "mozilla/MoveOnlyFunction.h" +#include "mozilla/RefPtr.h" +#include "nsISupports.h" +#include "nsTString.h" + +#ifdef DEBUG +# include "mozilla/Mutex.h" +#endif + +namespace mozilla::dom { + +/* + * If you want to play with a DOM Worker, you must know that it can go away + * at any time if nothing prevents its shutting down. This documentation helps + * to understand how to play with DOM Workers correctly. + * + * There are several reasons why a DOM Worker could go away. Here is the + * complete list: + * + * a. GC/CC - If the DOM Worker thread is idle and the Worker object is garbage + * collected, it goes away. + * b. The worker script can call self.close() + * c. The Worker object calls worker.terminate() + * d. Firefox is shutting down. + * + * When a DOM Worker goes away, it does several steps. See more in + * WorkerStatus.h. The DOM Worker thread will basically stop scheduling + * WorkerRunnables, and eventually WorkerControlRunnables. But if there is + * something preventing the shutting down, it will always possible to dispatch + * WorkerControlRunnables. Of course, at some point, the worker _must_ be + * released, otherwise firefox will leak it and the browser shutdown will hang. + * + * WeakWorkerRef is a refcounted, NON thread-safe object. + * + * From this object, you can obtain a WorkerPrivate, calling + * WeakWorkerRef::GetPrivate(). It returns nullptr if the worker is shutting + * down or if it is already gone away. + * + * If you want to know when a DOM Worker starts the shutting down procedure, + * pass a callback to the mozilla::dom::WeakWorkerRef::Create() method. + * Your function will be called. Note that _after_ the callback, + * WeakWorkerRef::GetPrivate() will return nullptr. + * + * How to keep a DOM Worker alive? + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * If you need to keep the worker alive, you must use StrongWorkerRef. + * You can have this refcounted, NON thread-safe object, calling + * mozilla::dom::StrongWorkerRef::Create(WorkerPrivate* aWorkerPrivate); + * + * If you have a StrongWorkerRef: + * a. the DOM Worker is kept alive. + * b. you can have access to the WorkerPrivate, calling: Private(). + * c. WorkerControlRunnable can be dispatched. + * + * Note that the DOM Worker shutdown can start at any time, but having a + * StrongWorkerRef prevents the full shutdown. Also with StrongWorkerRef, you + * can pass a callback when calling mozilla::dom::StrongWorkerRef::Create(). + * + * When the DOM Worker shutdown starts, WorkerRunnable cannot be dispatched + * anymore. At this point, you should dispatch WorkerControlRunnable just to + * release resources. + * + * How to have a thread-safe DOM Worker reference? + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * Sometimes you need to play with threads and you need a thread-safe worker + * reference. ThreadSafeWorkerRef is what you want. + * + * Just because this object can be sent to different threads, we don't allow the + * setting of a callback. It would be confusing. + * + * ThreadSafeWorkerRef can be destroyed in any thread. Internally it keeps a + * reference to its StrongWorkerRef creator and this ref will be dropped on the + * correct thread when the ThreadSafeWorkerRef is deleted. + * + * IPC WorkerRef + * ~~~~~~~~~~~~~ + * + * IPDL protocols require a correct shutdown sequence. Because of this, they + * need a special configuration: + * 1. they need to be informed when the Worker starts the shutting down + * 2. they don't want to prevent the shutdown + * 3. but at the same time, they need to block the shutdown until the WorkerRef + * is not longer alive. + * + * Point 1 is a standard feature of WorkerRef; point 2 is similar to + * WeakWorkerRef; point 3 is similar to StrongWorkerRef. + * + * You can create a special IPC WorkerRef using this static method: + * mozilla::dom::IPCWorkerRef::Create(WorkerPrivate* aWorkerPrivate, + * const char* * aName); + */ + +class WorkerPrivate; +class StrongWorkerRef; +class ThreadSafeWorkerRef; + +#ifdef DEBUG // In debug mode, provide a way for clients to annotate WorkerRefs +# define SET_WORKERREF_DEBUG_STATUS(workerref, str) \ + ((workerref)->DebugSetWorkerRefStatus(str)) +# define GET_WORKERREF_DEBUG_STATUS(workerref) \ + ((workerref)->DebugGetWorkerRefStatus()) +#else +# define SET_WORKERREF_DEBUG_STATUS(workerref, str) (void()) +# define GET_WORKERREF_DEBUG_STATUS(workerref) (EmptyCString()) +#endif + +class WorkerRef { + friend class WorkerPrivate; + + public: + NS_INLINE_DECL_REFCOUNTING(WorkerRef) + +#ifdef DEBUG + mutable Mutex mDebugMutex; + nsCString mDebugStatus MOZ_GUARDED_BY(mDebugMutex); + + void DebugSetWorkerRefStatus(const nsCString& aStatus) { + MutexAutoLock lock(mDebugMutex); + mDebugStatus = aStatus; + } + + const nsCString DebugGetWorkerRefStatus() const { + MutexAutoLock lock(mDebugMutex); + return mDebugStatus; + } +#endif + + protected: + WorkerRef(WorkerPrivate* aWorkerPrivate, const char* aName, + bool aIsPreventingShutdown); + virtual ~WorkerRef(); + + virtual void Notify(); + + bool HoldWorker(WorkerStatus aStatus); + void ReleaseWorker(); + + bool IsPreventingShutdown() const { return mIsPreventingShutdown; } + + const char* Name() const { return mName; } + + WorkerPrivate* mWorkerPrivate; + + MoveOnlyFunction<void()> mCallback; + const char* const mName; + const bool mIsPreventingShutdown; + + // True if this WorkerRef has been added to a WorkerPrivate. + bool mHolding; +}; + +class WeakWorkerRef final : public WorkerRef { + public: + static already_AddRefed<WeakWorkerRef> Create( + WorkerPrivate* aWorkerPrivate, + MoveOnlyFunction<void()>&& aCallback = nullptr); + + WorkerPrivate* GetPrivate() const; + + // This can be called on any thread. It's racy and, in general, the wrong + // choice. + WorkerPrivate* GetUnsafePrivate() const; + + private: + explicit WeakWorkerRef(WorkerPrivate* aWorkerPrivate); + ~WeakWorkerRef(); + + void Notify() override; +}; + +class StrongWorkerRef final : public WorkerRef { + public: + static already_AddRefed<StrongWorkerRef> Create( + WorkerPrivate* aWorkerPrivate, const char* aName, + MoveOnlyFunction<void()>&& aCallback = nullptr); + + // This function creates a StrongWorkerRef even when in the Canceling state of + // the worker's lifecycle. It's intended to be used by system code, e.g. code + // that needs to perform IPC. + // + // This method should only be used in cases where the StrongWorkerRef will be + // used for an extremely bounded duration that cannot be impacted by content. + // For example, IPCStreams use this type of ref in order to immediately + // migrate to an actor on another thread. Whether the IPCStream ever actually + // is streamed does not matter; the ref will be dropped once the new actor is + // created. For this reason, this method does not take a callback. It's + // expected and required that callers will drop the reference when they are + // done. + static already_AddRefed<StrongWorkerRef> CreateForcibly( + WorkerPrivate* aWorkerPrivate, const char* aName); + + WorkerPrivate* Private() const; + + private: + friend class WeakWorkerRef; + friend class ThreadSafeWorkerRef; + + static already_AddRefed<StrongWorkerRef> CreateImpl( + WorkerPrivate* aWorkerPrivate, const char* aName, + WorkerStatus aFailStatus); + + StrongWorkerRef(WorkerPrivate* aWorkerPrivate, const char* aName); + ~StrongWorkerRef(); +}; + +class ThreadSafeWorkerRef final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ThreadSafeWorkerRef) + + explicit ThreadSafeWorkerRef(StrongWorkerRef* aRef); + + WorkerPrivate* Private() const; + +#ifdef DEBUG + RefPtr<StrongWorkerRef>& Ref() { return mRef; } +#endif + + private: + friend class StrongWorkerRef; + + ~ThreadSafeWorkerRef(); + + RefPtr<StrongWorkerRef> mRef; +}; + +class IPCWorkerRef final : public WorkerRef { + public: + static already_AddRefed<IPCWorkerRef> Create( + WorkerPrivate* aWorkerPrivate, const char* aName, + MoveOnlyFunction<void()>&& aCallback = nullptr); + + WorkerPrivate* Private() const; + + void SetActorCount(uint32_t aCount); + + private: + IPCWorkerRef(WorkerPrivate* aWorkerPrivate, const char* aName); + ~IPCWorkerRef(); + + // The count of background actors which binding with this IPCWorkerRef. + uint32_t mActorCount; +}; + +// Template class to keep an Actor pointer, as a raw pointer, in a ref-counted +// way when passed to lambdas. +template <class ActorPtr> +class IPCWorkerRefHelper final { + public: + NS_INLINE_DECL_REFCOUNTING(IPCWorkerRefHelper); + + explicit IPCWorkerRefHelper(ActorPtr* aActor) : mActor(aActor) {} + + ActorPtr* Actor() const { return mActor; } + + private: + ~IPCWorkerRefHelper() = default; + + // Raw pointer + ActorPtr* mActor; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_workers_WorkerRef_h */ diff --git a/dom/workers/WorkerRunnable.cpp b/dom/workers/WorkerRunnable.cpp new file mode 100644 index 0000000000..ff2178d16e --- /dev/null +++ b/dom/workers/WorkerRunnable.cpp @@ -0,0 +1,705 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerRunnable.h" + +#include "WorkerScope.h" +#include "js/RootingAPI.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/Assertions.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Logging.h" +#include "mozilla/Maybe.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/Worker.h" +#include "mozilla/dom/WorkerCommon.h" +#include "nsDebug.h" +#include "nsGlobalWindowInner.h" +#include "nsID.h" +#include "nsIEventTarget.h" +#include "nsIGlobalObject.h" +#include "nsIRunnable.h" +#include "nsThreadUtils.h" +#include "nsWrapperCacheInlines.h" + +namespace mozilla::dom { + +static mozilla::LazyLogModule sWorkerRunnableLog("WorkerRunnable"); + +#ifdef LOG +# undef LOG +#endif +#define LOG(args) MOZ_LOG(sWorkerRunnableLog, LogLevel::Verbose, args); + +namespace { + +const nsIID kWorkerRunnableIID = { + 0x320cc0b5, + 0xef12, + 0x4084, + {0x88, 0x6e, 0xca, 0x6a, 0x81, 0xe4, 0x1d, 0x68}}; + +} // namespace + +#ifdef DEBUG +WorkerRunnable::WorkerRunnable(WorkerPrivate* aWorkerPrivate, const char* aName, + Target aTarget) + : mWorkerPrivate(aWorkerPrivate), + mTarget(aTarget), +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + mName(aName), +# endif + mCallingCancelWithinRun(false) { + LOG(("WorkerRunnable::WorkerRunnable [%p]", this)); + MOZ_ASSERT(aWorkerPrivate); +} +#endif + +bool WorkerRunnable::IsDebuggerRunnable() const { return false; } + +nsIGlobalObject* WorkerRunnable::DefaultGlobalObject() const { + if (IsDebuggerRunnable()) { + return mWorkerPrivate->DebuggerGlobalScope(); + } else { + return mWorkerPrivate->GlobalScope(); + } +} + +bool WorkerRunnable::PreDispatch(WorkerPrivate* aWorkerPrivate) { +#ifdef DEBUG + MOZ_ASSERT(aWorkerPrivate); + + switch (mTarget) { + case ParentThread: + aWorkerPrivate->AssertIsOnWorkerThread(); + break; + + case WorkerThread: + aWorkerPrivate->AssertIsOnParentThread(); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown behavior!"); + } +#endif + return true; +} + +bool WorkerRunnable::Dispatch() { + bool ok = PreDispatch(mWorkerPrivate); + if (ok) { + ok = DispatchInternal(); + } + PostDispatch(mWorkerPrivate, ok); + return ok; +} + +bool WorkerRunnable::DispatchInternal() { + LOG(("WorkerRunnable::DispatchInternal [%p]", this)); + RefPtr<WorkerRunnable> runnable(this); + + if (mTarget == WorkerThread) { + if (IsDebuggerRunnable()) { + return NS_SUCCEEDED( + mWorkerPrivate->DispatchDebuggerRunnable(runnable.forget())); + } else { + return NS_SUCCEEDED(mWorkerPrivate->Dispatch(runnable.forget())); + } + } + + MOZ_ASSERT(mTarget == ParentThread); + + if (WorkerPrivate* parent = mWorkerPrivate->GetParent()) { + return NS_SUCCEEDED(parent->Dispatch(runnable.forget())); + } + + if (IsDebuggeeRunnable()) { + RefPtr<WorkerDebuggeeRunnable> debuggeeRunnable = + runnable.forget().downcast<WorkerDebuggeeRunnable>(); + return NS_SUCCEEDED(mWorkerPrivate->DispatchDebuggeeToMainThread( + debuggeeRunnable.forget(), NS_DISPATCH_NORMAL)); + } + + return NS_SUCCEEDED(mWorkerPrivate->DispatchToMainThread(runnable.forget())); +} + +void WorkerRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) { + MOZ_ASSERT(aWorkerPrivate); + +#ifdef DEBUG + switch (mTarget) { + case ParentThread: + aWorkerPrivate->AssertIsOnWorkerThread(); + break; + + case WorkerThread: + aWorkerPrivate->AssertIsOnParentThread(); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown behavior!"); + } +#endif +} + +bool WorkerRunnable::PreRun(WorkerPrivate* aWorkerPrivate) { return true; } + +void WorkerRunnable::PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWorkerPrivate); + +#ifdef DEBUG + switch (mTarget) { + case ParentThread: + aWorkerPrivate->AssertIsOnParentThread(); + break; + + case WorkerThread: + aWorkerPrivate->AssertIsOnWorkerThread(); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown behavior!"); + } +#endif +} + +// static +WorkerRunnable* WorkerRunnable::FromRunnable(nsIRunnable* aRunnable) { + MOZ_ASSERT(aRunnable); + + WorkerRunnable* runnable; + nsresult rv = aRunnable->QueryInterface(kWorkerRunnableIID, + reinterpret_cast<void**>(&runnable)); + if (NS_FAILED(rv)) { + return nullptr; + } + + MOZ_ASSERT(runnable); + return runnable; +} + +NS_IMPL_ADDREF(WorkerRunnable) +NS_IMPL_RELEASE(WorkerRunnable) + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY +NS_IMETHODIMP +WorkerRunnable::GetName(nsACString& aName) { + if (mName) { + aName.AssignASCII(mName); + } else { + aName.Truncate(); + } + return NS_OK; +} +#endif + +NS_INTERFACE_MAP_BEGIN(WorkerRunnable) + NS_INTERFACE_MAP_ENTRY(nsIRunnable) +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_INTERFACE_MAP_ENTRY(nsINamed) +#endif + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRunnable) + // kWorkerRunnableIID is special in that it does not AddRef its result. + if (aIID.Equals(kWorkerRunnableIID)) { + *aInstancePtr = this; + return NS_OK; + } else +NS_INTERFACE_MAP_END + +NS_IMETHODIMP +WorkerRunnable::Run() { + LOG(("WorkerRunnable::Run [%p]", this)); + bool targetIsWorkerThread = mTarget == WorkerThread; + + if (targetIsWorkerThread) { + // On a worker thread, a WorkerRunnable should only run when there is an + // underlying WorkerThreadPrimaryRunnable active, which means we should + // find a CycleCollectedJSContext. + if (!CycleCollectedJSContext::Get()) { +#if (defined(MOZ_COLLECTING_RUNNABLE_TELEMETRY) && defined(NIGHTLY_BUILD)) + // We will only leak the static name string of the WorkerRunnable type + // we are trying to execute. + MOZ_CRASH_UNSAFE_PRINTF( + "Runnable '%s' executed after WorkerThreadPrimaryRunnable ended.", + this->mName); +#endif + return NS_OK; + } + } + +#ifdef DEBUG + if (targetIsWorkerThread) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } else { + MOZ_ASSERT(mTarget == ParentThread); + mWorkerPrivate->AssertIsOnParentThread(); + } +#endif + + if (targetIsWorkerThread && !mCallingCancelWithinRun && + mWorkerPrivate->CancelBeforeWorkerScopeConstructed()) { + mCallingCancelWithinRun = true; + Cancel(); + mCallingCancelWithinRun = false; + return NS_OK; + } + + bool result = PreRun(mWorkerPrivate); + if (!result) { + MOZ_ASSERT(targetIsWorkerThread, + "The only PreRun implementation that can fail is " + "ScriptExecutorRunnable"); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!JS_IsExceptionPending(mWorkerPrivate->GetJSContext())); + // We can't enter a useful realm on the JSContext here; just pass it + // in as-is. + PostRun(mWorkerPrivate->GetJSContext(), mWorkerPrivate, false); + return NS_ERROR_FAILURE; + } + + // Track down the appropriate global, if any, to use for the AutoEntryScript. + nsCOMPtr<nsIGlobalObject> globalObject; + bool isMainThread = !targetIsWorkerThread && !mWorkerPrivate->GetParent(); + MOZ_ASSERT(isMainThread == NS_IsMainThread()); + RefPtr<WorkerPrivate> kungFuDeathGrip; + if (targetIsWorkerThread) { + globalObject = mWorkerPrivate->GetCurrentEventLoopGlobal(); + if (!globalObject) { + globalObject = DefaultGlobalObject(); + // Our worker thread may not be in a good state here if there is no + // JSContext avaliable. The way this manifests itself is that + // globalObject ends up null (though it's not clear to me how we can be + // running runnables at all when DefaultGlobalObject() is returning + // false!) and then when we try to init the AutoJSAPI either + // CycleCollectedJSContext::Get() returns null or it has a null JSContext. + // In any case, we used to have a check for + // GetCurrentWorkerThreadJSContext() being non-null here and that seems to + // avoid the problem, so let's keep doing that check even if we don't need + // the JSContext here at all. + if (NS_WARN_IF(!globalObject && !GetCurrentWorkerThreadJSContext())) { + return NS_ERROR_FAILURE; + } + } + + // We may still not have a globalObject here: in the case of + // CompileScriptRunnable, we don't actually create the global object until + // we have the script data, which happens in a syncloop under + // CompileScriptRunnable::WorkerRun, so we can't assert that it got created + // in the PreRun call above. + } else { + kungFuDeathGrip = mWorkerPrivate; + if (isMainThread) { + globalObject = nsGlobalWindowInner::Cast(mWorkerPrivate->GetWindow()); + } else { + globalObject = mWorkerPrivate->GetParent()->GlobalScope(); + } + } + + // We might run script as part of WorkerRun, so we need an AutoEntryScript. + // This is part of the HTML spec for workers at: + // http://www.whatwg.org/specs/web-apps/current-work/#run-a-worker + // If we don't have a globalObject we have to use an AutoJSAPI instead, but + // this is OK as we won't be running script in these circumstances. + Maybe<mozilla::dom::AutoJSAPI> maybeJSAPI; + Maybe<mozilla::dom::AutoEntryScript> aes; + JSContext* cx; + AutoJSAPI* jsapi; + if (globalObject) { + aes.emplace(globalObject, "Worker runnable", isMainThread); + jsapi = aes.ptr(); + cx = aes->cx(); + } else { + maybeJSAPI.emplace(); + maybeJSAPI->Init(); + jsapi = maybeJSAPI.ptr(); + cx = jsapi->cx(); + } + + // Note that we can't assert anything about + // mWorkerPrivate->ParentEventTargetRef()->GetWrapper() + // existing, since it may in fact have been GCed (and we may be one of the + // runnables cleaning up the worker as a result). + + // If we are on the parent thread and that thread is not the main thread, + // then we must be a dedicated worker (because there are no + // Shared/ServiceWorkers whose parent is itself a worker) and then we + // definitely have a globalObject. If it _is_ the main thread, globalObject + // can be null for workers started from JSMs or other non-window contexts, + // sadly. + MOZ_ASSERT_IF(!targetIsWorkerThread && !isMainThread, + mWorkerPrivate->IsDedicatedWorker() && globalObject); + + // If we're on the parent thread we might be in a null realm in the + // situation described above when globalObject is null. Make sure to enter + // the realm of the worker's reflector if there is one. There might + // not be one if we're just starting to compile the script for this worker. + Maybe<JSAutoRealm> ar; + if (!targetIsWorkerThread && mWorkerPrivate->IsDedicatedWorker() && + mWorkerPrivate->ParentEventTargetRef()->GetWrapper()) { + JSObject* wrapper = mWorkerPrivate->ParentEventTargetRef()->GetWrapper(); + + // If we're on the parent thread and have a reflector and a globalObject, + // then the realms of cx, globalObject, and the worker's reflector + // should all match. + MOZ_ASSERT_IF(globalObject, + js::GetNonCCWObjectRealm(wrapper) == js::GetContextRealm(cx)); + MOZ_ASSERT_IF(globalObject, + js::GetNonCCWObjectRealm(wrapper) == + js::GetNonCCWObjectRealm( + globalObject->GetGlobalJSObjectPreserveColor())); + + // If we're on the parent thread and have a reflector, then our + // JSContext had better be either in the null realm (and hence + // have no globalObject) or in the realm of our reflector. + MOZ_ASSERT(!js::GetContextRealm(cx) || + js::GetNonCCWObjectRealm(wrapper) == js::GetContextRealm(cx), + "Must either be in the null compartment or in our reflector " + "compartment"); + + ar.emplace(cx, wrapper); + } + + MOZ_ASSERT(!jsapi->HasException()); + result = WorkerRun(cx, mWorkerPrivate); + jsapi->ReportException(); + + // We can't even assert that this didn't create our global, since in the case + // of CompileScriptRunnable it _does_. + + // It would be nice to avoid passing a JSContext to PostRun, but in the case + // of ScriptExecutorRunnable we need to know the current compartment on the + // JSContext (the one we set up based on the global returned from PreRun) so + // that we can sanely do exception reporting. In particular, we want to make + // sure that we do our JS_SetPendingException while still in that compartment, + // because otherwise we might end up trying to create a cross-compartment + // wrapper when we try to move the JS exception from our runnable's + // ErrorResult to the JSContext, and that's not desirable in this case. + // + // We _could_ skip passing a JSContext here and then in + // ScriptExecutorRunnable::PostRun end up grabbing it from the WorkerPrivate + // and looking at its current compartment. But that seems like slightly weird + // action-at-a-distance... + // + // In any case, we do NOT try to change the compartment on the JSContext at + // this point; in the one case in which we could do that + // (CompileScriptRunnable) it actually doesn't matter which compartment we're + // in for PostRun. + PostRun(cx, mWorkerPrivate, result); + MOZ_ASSERT(!jsapi->HasException()); + + return result ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult WorkerRunnable::Cancel() { + LOG(("WorkerRunnable::Cancel [%p]", this)); + return NS_OK; +} + +void WorkerDebuggerRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) {} + +WorkerSyncRunnable::WorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget, + const char* aName) + : WorkerRunnable(aWorkerPrivate, aName, WorkerThread), + mSyncLoopTarget(aSyncLoopTarget) { +#ifdef DEBUG + if (mSyncLoopTarget) { + mWorkerPrivate->AssertValidSyncLoop(mSyncLoopTarget); + } +#endif +} + +WorkerSyncRunnable::WorkerSyncRunnable( + WorkerPrivate* aWorkerPrivate, nsCOMPtr<nsIEventTarget>&& aSyncLoopTarget, + const char* aName) + : WorkerRunnable(aWorkerPrivate, aName, WorkerThread), + mSyncLoopTarget(std::move(aSyncLoopTarget)) { +#ifdef DEBUG + if (mSyncLoopTarget) { + mWorkerPrivate->AssertValidSyncLoop(mSyncLoopTarget); + } +#endif +} + +WorkerSyncRunnable::~WorkerSyncRunnable() = default; + +bool WorkerSyncRunnable::DispatchInternal() { + if (mSyncLoopTarget) { + RefPtr<WorkerSyncRunnable> runnable(this); + return NS_SUCCEEDED( + mSyncLoopTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); + } + + return WorkerRunnable::DispatchInternal(); +} + +void MainThreadWorkerSyncRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) {} + +MainThreadStopSyncLoopRunnable::MainThreadStopSyncLoopRunnable( + WorkerPrivate* aWorkerPrivate, nsCOMPtr<nsIEventTarget>&& aSyncLoopTarget, + nsresult aResult) + : WorkerSyncRunnable(aWorkerPrivate, std::move(aSyncLoopTarget)), + mResult(aResult) { + LOG(("MainThreadStopSyncLoopRunnable::MainThreadStopSyncLoopRunnable [%p]", + this)); + + AssertIsOnMainThread(); +#ifdef DEBUG + mWorkerPrivate->AssertValidSyncLoop(mSyncLoopTarget); +#endif +} + +nsresult MainThreadStopSyncLoopRunnable::Cancel() { + LOG(("MainThreadStopSyncLoopRunnable::Cancel [%p]", this)); + nsresult rv = Run(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Run() failed"); + + return rv; +} + +bool MainThreadStopSyncLoopRunnable::WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mSyncLoopTarget); + + nsCOMPtr<nsIEventTarget> syncLoopTarget; + mSyncLoopTarget.swap(syncLoopTarget); + + aWorkerPrivate->StopSyncLoop(syncLoopTarget, mResult); + return true; +} + +bool MainThreadStopSyncLoopRunnable::DispatchInternal() { + MOZ_ASSERT(mSyncLoopTarget); + + RefPtr<MainThreadStopSyncLoopRunnable> runnable(this); + return NS_SUCCEEDED( + mSyncLoopTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); +} + +void MainThreadStopSyncLoopRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) {} + +#ifdef DEBUG +WorkerControlRunnable::WorkerControlRunnable(WorkerPrivate* aWorkerPrivate, + const char* aName, Target aTarget) + : WorkerRunnable(aWorkerPrivate, aName, aTarget) { + MOZ_ASSERT(aWorkerPrivate); +} +#endif + +nsresult WorkerControlRunnable::Cancel() { + LOG(("WorkerControlRunnable::Cancel [%p]", this)); + if (NS_FAILED(Run())) { + NS_WARNING("WorkerControlRunnable::Run() failed."); + } + + return NS_OK; +} + +bool WorkerControlRunnable::DispatchInternal() { + RefPtr<WorkerControlRunnable> runnable(this); + + if (mTarget == WorkerThread) { + return NS_SUCCEEDED( + mWorkerPrivate->DispatchControlRunnable(runnable.forget())); + } + + if (WorkerPrivate* parent = mWorkerPrivate->GetParent()) { + return NS_SUCCEEDED(parent->DispatchControlRunnable(runnable.forget())); + } + + return NS_SUCCEEDED(mWorkerPrivate->DispatchToMainThread(runnable.forget())); +} + +WorkerMainThreadRunnable::WorkerMainThreadRunnable( + WorkerPrivate* aWorkerPrivate, const nsACString& aTelemetryKey) + : mozilla::Runnable("dom::WorkerMainThreadRunnable"), + mWorkerPrivate(aWorkerPrivate), + mTelemetryKey(aTelemetryKey) { + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +WorkerMainThreadRunnable::~WorkerMainThreadRunnable() = default; + +void WorkerMainThreadRunnable::Dispatch(WorkerStatus aFailStatus, + mozilla::ErrorResult& aRv) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + TimeStamp startTime = TimeStamp::NowLoRes(); + + AutoSyncLoopHolder syncLoop(mWorkerPrivate, aFailStatus); + + mSyncLoopTarget = syncLoop.GetSerialEventTarget(); + if (!mSyncLoopTarget) { + // SyncLoop creation can fail if the worker is shutting down. + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + DebugOnly<nsresult> rv = mWorkerPrivate->DispatchToMainThread(this); + MOZ_ASSERT( + NS_SUCCEEDED(rv), + "Should only fail after xpcom-shutdown-threads and we're gone by then"); + + bool success = NS_SUCCEEDED(syncLoop.Run()); + + Telemetry::Accumulate( + Telemetry::SYNC_WORKER_OPERATION, mTelemetryKey, + static_cast<uint32_t>( + (TimeStamp::NowLoRes() - startTime).ToMilliseconds())); + + Unused << startTime; // Shut the compiler up. + + if (!success) { + aRv.ThrowUncatchableException(); + } +} + +NS_IMETHODIMP +WorkerMainThreadRunnable::Run() { + AssertIsOnMainThread(); + + // This shouldn't be necessary once we're better about making sure no workers + // are created during shutdown in earlier phases. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + bool runResult = MainThreadRun(); + + RefPtr<MainThreadStopSyncLoopRunnable> response = + new MainThreadStopSyncLoopRunnable(mWorkerPrivate, + std::move(mSyncLoopTarget), + runResult ? NS_OK : NS_ERROR_FAILURE); + + MOZ_ALWAYS_TRUE(response->Dispatch()); + + return NS_OK; +} + +bool WorkerSameThreadRunnable::PreDispatch(WorkerPrivate* aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + return true; +} + +void WorkerSameThreadRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) { + aWorkerPrivate->AssertIsOnWorkerThread(); +} + +WorkerProxyToMainThreadRunnable::WorkerProxyToMainThreadRunnable() + : mozilla::Runnable("dom::WorkerProxyToMainThreadRunnable") {} + +WorkerProxyToMainThreadRunnable::~WorkerProxyToMainThreadRunnable() = default; + +bool WorkerProxyToMainThreadRunnable::Dispatch(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + aWorkerPrivate, "WorkerProxyToMainThreadRunnable"); + if (NS_WARN_IF(!workerRef)) { + RunBackOnWorkerThreadForCleanup(aWorkerPrivate); + return false; + } + + MOZ_ASSERT(!mWorkerRef); + mWorkerRef = new ThreadSafeWorkerRef(workerRef); + + if (ForMessaging() + ? NS_WARN_IF(NS_FAILED( + aWorkerPrivate->DispatchToMainThreadForMessaging(this))) + : NS_WARN_IF(NS_FAILED(aWorkerPrivate->DispatchToMainThread(this)))) { + ReleaseWorker(); + RunBackOnWorkerThreadForCleanup(aWorkerPrivate); + return false; + } + + return true; +} + +NS_IMETHODIMP +WorkerProxyToMainThreadRunnable::Run() { + AssertIsOnMainThread(); + RunOnMainThread(mWorkerRef->Private()); + PostDispatchOnMainThread(); + return NS_OK; +} + +void WorkerProxyToMainThreadRunnable::PostDispatchOnMainThread() { + class ReleaseRunnable final : public MainThreadWorkerControlRunnable { + RefPtr<WorkerProxyToMainThreadRunnable> mRunnable; + + public: + ReleaseRunnable(WorkerPrivate* aWorkerPrivate, + WorkerProxyToMainThreadRunnable* aRunnable) + : MainThreadWorkerControlRunnable(aWorkerPrivate), + mRunnable(aRunnable) { + MOZ_ASSERT(aRunnable); + } + + virtual nsresult Cancel() override { + Unused << WorkerRun(nullptr, mWorkerPrivate); + return NS_OK; + } + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + if (mRunnable) { + mRunnable->RunBackOnWorkerThreadForCleanup(aWorkerPrivate); + + // Let's release the worker thread. + mRunnable->ReleaseWorker(); + mRunnable = nullptr; + } + + return true; + } + + private: + ~ReleaseRunnable() = default; + }; + + RefPtr<WorkerControlRunnable> runnable = + new ReleaseRunnable(mWorkerRef->Private(), this); + Unused << NS_WARN_IF(!runnable->Dispatch()); +} + +void WorkerProxyToMainThreadRunnable::ReleaseWorker() { mWorkerRef = nullptr; } + +bool WorkerDebuggeeRunnable::PreDispatch(WorkerPrivate* aWorkerPrivate) { + if (mTarget == ParentThread) { + RefPtr<StrongWorkerRef> strongRef = StrongWorkerRef::Create( + aWorkerPrivate, "WorkerDebuggeeRunnable::mSender"); + if (!strongRef) { + return false; + } + + mSender = new ThreadSafeWorkerRef(strongRef); + } + + return WorkerRunnable::PreDispatch(aWorkerPrivate); +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerRunnable.h b/dom/workers/WorkerRunnable.h new file mode 100644 index 0000000000..d133f11ea2 --- /dev/null +++ b/dom/workers/WorkerRunnable.h @@ -0,0 +1,503 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_workerrunnable_h__ +#define mozilla_dom_workers_workerrunnable_h__ + +#include <cstdint> +#include <utility> +#include "MainThreadUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerStatus.h" +#include "nsCOMPtr.h" +#include "nsIRunnable.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsThreadUtils.h" +#include "nscore.h" + +struct JSContext; +class nsIEventTarget; +class nsIGlobalObject; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class WorkerPrivate; + +// Use this runnable to communicate from the worker to its parent or vice-versa. +class WorkerRunnable : public nsIRunnable +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + , + public nsINamed +#endif +{ + public: + enum Target { + // Target the main thread for top-level workers, otherwise target the + // WorkerThread of the worker's parent. + ParentThread, + + // Target the thread where the worker event loop runs. + WorkerThread, + }; + + protected: + // The WorkerPrivate that this runnable is associated with. + WorkerPrivate* mWorkerPrivate; + + // See above. + Target mTarget; + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + const char* mName = nullptr; +#endif + + private: + // Whether or not Cancel() is currently being called from inside the Run() + // method. Avoids infinite recursion when a subclass calls Run() from inside + // Cancel(). Only checked and modified on the target thread. + bool mCallingCancelWithinRun; + + public: + NS_DECL_THREADSAFE_ISUPPORTS +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_DECL_NSINAMED +#endif + + virtual nsresult Cancel(); + + // The return value is true if and only if both PreDispatch and + // DispatchInternal return true. + bool Dispatch(); + + // True if this runnable is handled by running JavaScript in some global that + // could possibly be a debuggee, and thus needs to be deferred when the target + // is paused in the debugger, until the JavaScript invocation in progress has + // run to completion. Examples are MessageEventRunnable and + // ReportErrorRunnable. These runnables are segregated into separate + // ThrottledEventQueues, which the debugger pauses. + // + // Note that debugger runnables do not fall in this category, since we don't + // support debugging the debugger server at the moment. + virtual bool IsDebuggeeRunnable() const { return false; } + + static WorkerRunnable* FromRunnable(nsIRunnable* aRunnable); + + protected: + WorkerRunnable(WorkerPrivate* aWorkerPrivate, + const char* aName = "WorkerRunnable", + Target aTarget = WorkerThread) +#ifdef DEBUG + ; +#else + : mWorkerPrivate(aWorkerPrivate), + mTarget(aTarget), +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + mName(aName), +# endif + mCallingCancelWithinRun(false) { + } +#endif + + // This class is reference counted. + virtual ~WorkerRunnable() = default; + + // Returns true if this runnable should be dispatched to the debugger queue, + // and false otherwise. + virtual bool IsDebuggerRunnable() const; + + nsIGlobalObject* DefaultGlobalObject() const; + + // By default asserts that Dispatch() is being called on the right thread + // (ParentThread if |mTarget| is WorkerThread). + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate); + + // By default asserts that Dispatch() is being called on the right thread + // (ParentThread if |mTarget| is WorkerThread). + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult); + + // May be implemented by subclasses if desired if they need to do some sort of + // setup before we try to set up our JSContext and compartment for real. + // Typically the only thing that should go in here is creation of the worker's + // global. + // + // If false is returned, WorkerRun will not be called at all. PostRun will + // still be called, with false passed for aRunResult. + virtual bool PreRun(WorkerPrivate* aWorkerPrivate); + + // Must be implemented by subclasses. Called on the target thread. The return + // value will be passed to PostRun(). The JSContext passed in here comes from + // an AutoJSAPI (or AutoEntryScript) that we set up on the stack. If + // mTarget is ParentThread, it is in the compartment of + // mWorkerPrivate's reflector (i.e. the worker object in the parent thread), + // unless that reflector is null, in which case it's in the compartment of the + // parent global (which is the compartment reflector would have been in), or + // in the null compartment if there is no parent global. For other mTarget + // values, we're running on the worker thread and aCx is in whatever + // compartment GetCurrentWorkerThreadJSContext() was in when + // nsIRunnable::Run() got called. This is actually important for cases when a + // runnable spins a syncloop and wants everything that happens during the + // syncloop to happen in the compartment that runnable set up (which may, for + // example, be a debugger sandbox compartment!). If aCx wasn't in a + // compartment to start with, aCx will be in either the debugger global's + // compartment or the worker's global's compartment depending on whether + // IsDebuggerRunnable() is true. + // + // Immediately after WorkerRun returns, the caller will assert that either it + // returns false or there is no exception pending on aCx. Then it will report + // any pending exceptions on aCx. + virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) = 0; + + // By default asserts that Run() (and WorkerRun()) were called on the correct + // thread. + // + // The aCx passed here is the same one as was passed to WorkerRun and is + // still in the same compartment. PostRun implementations must NOT leave an + // exception on the JSContext and must not run script, because the incoming + // JSContext may be in the null compartment. + virtual void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult); + + virtual bool DispatchInternal(); + + // Calling Run() directly is not supported. Just call Dispatch() and + // WorkerRun() will be called on the correct thread automatically. + NS_DECL_NSIRUNNABLE +}; + +// This runnable is used to send a message to a worker debugger. +class WorkerDebuggerRunnable : public WorkerRunnable { + protected: + explicit WorkerDebuggerRunnable(WorkerPrivate* aWorkerPrivate, + const char* aName = "WorkerDebuggerRunnable") + : WorkerRunnable(aWorkerPrivate, aName, WorkerThread) {} + + virtual ~WorkerDebuggerRunnable() = default; + + private: + virtual bool IsDebuggerRunnable() const override { return true; } + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) final { + AssertIsOnMainThread(); + + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override; +}; + +// This runnable is used to send a message directly to a worker's sync loop. +class WorkerSyncRunnable : public WorkerRunnable { + protected: + nsCOMPtr<nsIEventTarget> mSyncLoopTarget; + + // Passing null for aSyncLoopTarget is allowed and will result in the behavior + // of a normal WorkerRunnable. + WorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget, + const char* aName = "WorkerSyncRunnable"); + + WorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + nsCOMPtr<nsIEventTarget>&& aSyncLoopTarget, + const char* aName = "WorkerSyncRunnable"); + + virtual ~WorkerSyncRunnable(); + + virtual bool DispatchInternal() override; +}; + +// This runnable is identical to WorkerSyncRunnable except it is meant to be +// created on and dispatched from the main thread only. Its WorkerRun/PostRun +// will run on the worker thread. +class MainThreadWorkerSyncRunnable : public WorkerSyncRunnable { + protected: + // Passing null for aSyncLoopTarget is allowed and will result in the behavior + // of a normal WorkerRunnable. + MainThreadWorkerSyncRunnable( + WorkerPrivate* aWorkerPrivate, nsIEventTarget* aSyncLoopTarget, + const char* aName = "MainThreadWorkerSyncRunnable") + : WorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget, aName) { + AssertIsOnMainThread(); + } + + MainThreadWorkerSyncRunnable( + WorkerPrivate* aWorkerPrivate, nsCOMPtr<nsIEventTarget>&& aSyncLoopTarget, + const char* aName = "MainThreadWorkerSyncRunnable") + : WorkerSyncRunnable(aWorkerPrivate, std::move(aSyncLoopTarget), aName) { + AssertIsOnMainThread(); + } + + virtual ~MainThreadWorkerSyncRunnable() = default; + + private: + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + AssertIsOnMainThread(); + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override; +}; + +// This runnable is processed as soon as it is received by the worker, +// potentially running before previously queued runnables and perhaps even with +// other JS code executing on the stack. These runnables must not alter the +// state of the JS runtime and should only twiddle state values. +class WorkerControlRunnable : public WorkerRunnable { + friend class WorkerPrivate; + + protected: + WorkerControlRunnable(WorkerPrivate* aWorkerPrivate, + const char* aName = "WorkerControlRunnable", + Target aTarget = WorkerThread) +#ifdef DEBUG + ; +#else + : WorkerRunnable(aWorkerPrivate, aName, aTarget) { + } +#endif + + virtual ~WorkerControlRunnable() = default; + + nsresult Cancel() override; + + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(WorkerControlRunnable, WorkerRunnable) + + private: + virtual bool DispatchInternal() override; + + // Should only be called by WorkerPrivate::DoRunLoop. + using WorkerRunnable::Cancel; +}; + +// A convenience class for WorkerRunnables that are originated on the main +// thread. +class MainThreadWorkerRunnable : public WorkerRunnable { + protected: + explicit MainThreadWorkerRunnable( + WorkerPrivate* aWorkerPrivate, + const char* aName = "MainThreadWorkerRunnable") + : WorkerRunnable(aWorkerPrivate, aName, WorkerThread) { + AssertIsOnMainThread(); + } + + virtual ~MainThreadWorkerRunnable() = default; + + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + AssertIsOnMainThread(); + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + AssertIsOnMainThread(); + } +}; + +// A convenience class for WorkerControlRunnables that originate on the main +// thread. +class MainThreadWorkerControlRunnable : public WorkerControlRunnable { + protected: + explicit MainThreadWorkerControlRunnable( + WorkerPrivate* aWorkerPrivate, + const char* aName = "MainThreadWorkerControlRunnable") + : WorkerControlRunnable(aWorkerPrivate, aName, WorkerThread) {} + + virtual ~MainThreadWorkerControlRunnable() = default; + + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { + AssertIsOnMainThread(); + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override { + AssertIsOnMainThread(); + } +}; + +// A WorkerRunnable that should be dispatched from the worker to itself for +// async tasks. +// +// Async tasks will almost always want to use this since +// a WorkerSameThreadRunnable keeps the Worker from being GCed. +class WorkerSameThreadRunnable : public WorkerRunnable { + protected: + explicit WorkerSameThreadRunnable( + WorkerPrivate* aWorkerPrivate, + const char* aName = "WorkerSameThreadRunnable") + : WorkerRunnable(aWorkerPrivate, aName, WorkerThread) {} + + virtual ~WorkerSameThreadRunnable() = default; + + virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override; + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override; + + // We just delegate PostRun to WorkerRunnable, since it does exactly + // what we want. +}; + +// Base class for the runnable objects, which makes a synchronous call to +// dispatch the tasks from the worker thread to the main thread. +// +// Note that the derived class must override MainThreadRun. +class WorkerMainThreadRunnable : public Runnable { + protected: + WorkerPrivate* mWorkerPrivate; + nsCOMPtr<nsISerialEventTarget> mSyncLoopTarget; + const nsCString mTelemetryKey; + + explicit WorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + const nsACString& aTelemetryKey); + ~WorkerMainThreadRunnable(); + + virtual bool MainThreadRun() = 0; + + public: + // Dispatch the runnable to the main thread. If dispatch to main thread + // fails, or if the worker is in a state equal or greater of aFailStatus, an + // error will be reported on aRv. Normally you want to use 'Canceling' for + // aFailStatus, except if you want an infallible runnable. In this case, use + // 'Killing'. + // In that case the error MUST be propagated out to script. + void Dispatch(WorkerStatus aFailStatus, ErrorResult& aRv); + + private: + NS_IMETHOD Run() override; +}; + +// This runnable is an helper class for dispatching something from a worker +// thread to the main-thread and back to the worker-thread. During this +// operation, this class will keep the worker alive. +// The purpose of RunBackOnWorkerThreadForCleanup() must be used, as the name +// says, only to release resources, no JS has to be executed, no timers, or +// other things. The reason of such limitations is that, in order to execute +// this method in any condition (also when the worker is shutting down), a +// Control Runnable is used, and, this could generate a reordering of existing +// runnables. +class WorkerProxyToMainThreadRunnable : public Runnable { + protected: + WorkerProxyToMainThreadRunnable(); + + virtual ~WorkerProxyToMainThreadRunnable(); + + // First this method is called on the main-thread. + virtual void RunOnMainThread(WorkerPrivate* aWorkerPrivate) = 0; + + // After this second method is called on the worker-thread. + virtual void RunBackOnWorkerThreadForCleanup( + WorkerPrivate* aWorkerPrivate) = 0; + + public: + bool Dispatch(WorkerPrivate* aWorkerPrivate); + + virtual bool ForMessaging() const { return false; } + + private: + NS_IMETHOD Run() override; + + void PostDispatchOnMainThread(); + + void ReleaseWorker(); + + RefPtr<ThreadSafeWorkerRef> mWorkerRef; +}; + +// This runnable is used to stop a sync loop and it's meant to be used on the +// main-thread only. +class MainThreadStopSyncLoopRunnable : public WorkerSyncRunnable { + nsresult mResult; + + public: + // Passing null for aSyncLoopTarget is not allowed. + MainThreadStopSyncLoopRunnable(WorkerPrivate* aWorkerPrivate, + nsCOMPtr<nsIEventTarget>&& aSyncLoopTarget, + nsresult aResult); + + // By default StopSyncLoopRunnables cannot be canceled since they could leave + // a sync loop spinning forever. + nsresult Cancel() override; + + protected: + virtual ~MainThreadStopSyncLoopRunnable() = default; + + private: + bool PreDispatch(WorkerPrivate* aWorkerPrivate) final { + AssertIsOnMainThread(); + return true; + } + + virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override; + + virtual bool WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) override; + + bool DispatchInternal() final; +}; + +// Runnables handled by content JavaScript (MessageEventRunnable, JavaScript +// error reports, and so on) must not be delivered while that content is in the +// midst of being debugged; the debuggee must be allowed to complete its current +// JavaScript invocation and return to its own event loop. Only then is it +// prepared for messages sent from the worker. +// +// Runnables that need to be deferred in this way should inherit from this +// class. They will be routed to mMainThreadDebuggeeEventTarget, which is paused +// while the window is suspended, as it is whenever the debugger spins its +// nested event loop. When the debugger leaves its nested event loop, it resumes +// the window, so that mMainThreadDebuggeeEventTarget will resume delivering +// runnables from the worker when control returns to the main event loop. +// +// When a page enters the bfcache, it freezes all its workers. Since a frozen +// worker processes only control runnables, it doesn't take any special +// consideration to prevent WorkerDebuggeeRunnables sent from child to parent +// workers from running; they'll never run anyway. But WorkerDebuggeeRunnables +// from a top-level frozen worker to its parent window must not be delivered +// either, even as the main thread event loop continues to spin. Thus, freezing +// a top-level worker also pauses mMainThreadDebuggeeEventTarget. +class WorkerDebuggeeRunnable : public WorkerRunnable { + protected: + WorkerDebuggeeRunnable(WorkerPrivate* aWorkerPrivate, + const char* aName = "WorkerDebuggeeRunnable", + Target aTarget = ParentThread) + : WorkerRunnable(aWorkerPrivate, aName, aTarget) {} + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override; + + private: + // This override is deliberately private: it doesn't make sense to call it if + // we know statically that we are a WorkerDebuggeeRunnable. + bool IsDebuggeeRunnable() const override { return true; } + + // Runnables sent upwards, to the content window or parent worker, must keep + // their sender alive until they are delivered: they check back with the + // sender in case it has been terminated after having dispatched the runnable + // (in which case it should not be acted upon); and runnables sent to content + // wait until delivery to determine the target window, since + // WorkerPrivate::GetWindow may only be used on the main thread. + // + // Runnables sent downwards, from content to a worker or from a worker to a + // child, keep the sender alive because they are WorkerThread + // runnables, and so leave this null. + RefPtr<ThreadSafeWorkerRef> mSender; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_workerrunnable_h__ diff --git a/dom/workers/WorkerScope.cpp b/dom/workers/WorkerScope.cpp new file mode 100644 index 0000000000..159829f4a8 --- /dev/null +++ b/dom/workers/WorkerScope.cpp @@ -0,0 +1,1406 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WorkerScope.h" + +#include <stdio.h> +#include <new> +#include <utility> +#include "Crypto.h" +#include "GeckoProfiler.h" +#include "MainThreadUtils.h" +#include "ScriptLoader.h" +#include "js/CompilationAndEvaluation.h" +#include "js/CompileOptions.h" +#include "js/RealmOptions.h" +#include "js/RootingAPI.h" +#include "js/SourceText.h" +#include "js/Value.h" +#include "js/Wrapper.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/BaseProfilerMarkersPrerequisites.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/Logging.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/StaticAnalysisFunctions.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/CSPEvalChecker.h" +#include "mozilla/dom/CallbackDebuggerNotification.h" +#include "mozilla/dom/ClientSource.h" +#include "mozilla/dom/Clients.h" +#include "mozilla/dom/Console.h" +#include "mozilla/dom/DOMMozPromiseRequestHolder.h" +#include "mozilla/dom/DebuggerNotification.h" +#include "mozilla/dom/DebuggerNotificationBinding.h" +#include "mozilla/dom/DebuggerNotificationManager.h" +#include "mozilla/dom/DedicatedWorkerGlobalScopeBinding.h" +#include "mozilla/dom/DOMString.h" +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/FontFaceSet.h" +#include "mozilla/dom/IDBFactory.h" +#include "mozilla/dom/ImageBitmap.h" +#include "mozilla/dom/ImageBitmapSource.h" +#include "mozilla/dom/MessagePortBinding.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/WebTaskSchedulerWorker.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SerializedStackHolder.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/dom/ServiceWorkerRegistration.h" +#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/SharedWorkerGlobalScopeBinding.h" +#include "mozilla/dom/SimpleGlobalObject.h" +#include "mozilla/dom/TimeoutHandler.h" +#include "mozilla/dom/TestUtils.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerLocation.h" +#include "mozilla/dom/WorkerNavigator.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerDocumentListener.h" +#include "mozilla/dom/VsyncWorkerChild.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/dom/cache/Types.h" +#include "mozilla/extensions/ExtensionBrowser.h" +#include "mozilla/fallible.h" +#include "mozilla/gfx/Rect.h" +#include "nsAtom.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsGkAtoms.h" +#include "nsIEventTarget.h" +#include "nsIGlobalObject.h" +#include "nsIScriptError.h" +#include "nsISerialEventTarget.h" +#include "nsIWeakReference.h" +#include "nsJSUtils.h" +#include "nsLiteralString.h" +#include "nsQueryObject.h" +#include "nsReadableUtils.h" +#include "nsRFPService.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsTLiteralString.h" +#include "nsThreadUtils.h" +#include "nsWeakReference.h" +#include "nsWrapperCacheInlines.h" +#include "nscore.h" +#include "xpcpublic.h" + +#ifdef ANDROID +# include <android/log.h> +#endif + +#ifdef XP_WIN +# undef PostMessage +#endif + +using mozilla::dom::cache::CacheStorage; +using mozilla::dom::workerinternals::NamedWorkerGlobalScopeMixin; +using mozilla::ipc::BackgroundChild; +using mozilla::ipc::PBackgroundChild; +using mozilla::ipc::PrincipalInfo; + +namespace mozilla::dom { + +static mozilla::LazyLogModule sWorkerScopeLog("WorkerScope"); + +#ifdef LOG +# undef LOG +#endif +#define LOG(args) MOZ_LOG(sWorkerScopeLog, LogLevel::Debug, args); + +class WorkerScriptTimeoutHandler final : public ScriptTimeoutHandler { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WorkerScriptTimeoutHandler, + ScriptTimeoutHandler) + + WorkerScriptTimeoutHandler(JSContext* aCx, nsIGlobalObject* aGlobal, + const nsAString& aExpression) + : ScriptTimeoutHandler(aCx, aGlobal, aExpression) {} + + MOZ_CAN_RUN_SCRIPT virtual bool Call(const char* aExecutionReason) override; + + private: + virtual ~WorkerScriptTimeoutHandler() = default; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(WorkerScriptTimeoutHandler, + ScriptTimeoutHandler) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkerScriptTimeoutHandler) +NS_INTERFACE_MAP_END_INHERITING(ScriptTimeoutHandler) + +NS_IMPL_ADDREF_INHERITED(WorkerScriptTimeoutHandler, ScriptTimeoutHandler) +NS_IMPL_RELEASE_INHERITED(WorkerScriptTimeoutHandler, ScriptTimeoutHandler) + +bool WorkerScriptTimeoutHandler::Call(const char* aExecutionReason) { + nsAutoMicroTask mt; + AutoEntryScript aes(mGlobal, aExecutionReason, false); + + JSContext* cx = aes.cx(); + JS::CompileOptions options(cx); + options.setFileAndLine(mFileName.get(), mLineNo).setNoScriptRval(true); + options.setIntroductionType("domTimer"); + + JS::Rooted<JS::Value> unused(cx); + JS::SourceText<char16_t> srcBuf; + if (!srcBuf.init(cx, mExpr.BeginReading(), mExpr.Length(), + JS::SourceOwnership::Borrowed) || + !JS::Evaluate(cx, options, srcBuf, &unused)) { + if (!JS_IsExceptionPending(cx)) { + return false; + } + } + + return true; +}; + +namespace workerinternals { +void NamedWorkerGlobalScopeMixin::GetName(DOMString& aName) const { + aName.AsAString() = mName; +} +} // namespace workerinternals + +NS_IMPL_CYCLE_COLLECTION_CLASS(WorkerGlobalScopeBase) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WorkerGlobalScopeBase, + DOMEventTargetHelper) + tmp->AssertIsOnWorkerThread(); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsole) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mModuleLoader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSerialEventTarget) + tmp->TraverseObjectsInGlobal(cb); + // If we already exited WorkerThreadPrimaryRunnable, we will find it + // nullptr and there is nothing left to do here on the WorkerPrivate, + // in particular the timeouts have already been canceled and unlinked. + if (tmp->mWorkerPrivate) { + tmp->mWorkerPrivate->TraverseTimeouts(cb); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WorkerGlobalScopeBase, + DOMEventTargetHelper) + tmp->AssertIsOnWorkerThread(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mModuleLoader) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSerialEventTarget) + tmp->UnlinkObjectsInGlobal(); + // If we already exited WorkerThreadPrimaryRunnable, we will find it + // nullptr and there is nothing left to do here on the WorkerPrivate, + // in particular the timeouts have already been canceled and unlinked. + if (tmp->mWorkerPrivate) { + tmp->mWorkerPrivate->UnlinkTimeouts(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(WorkerGlobalScopeBase, + DOMEventTargetHelper) + tmp->AssertIsOnWorkerThread(); +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(WorkerGlobalScopeBase, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(WorkerGlobalScopeBase, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkerGlobalScopeBase) + NS_INTERFACE_MAP_ENTRY(nsIGlobalObject) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +WorkerGlobalScopeBase::WorkerGlobalScopeBase( + WorkerPrivate* aWorkerPrivate, UniquePtr<ClientSource> aClientSource) + : mWorkerPrivate(aWorkerPrivate), + mClientSource(std::move(aClientSource)), + mSerialEventTarget(aWorkerPrivate->HybridEventTarget()) { + LOG(("WorkerGlobalScopeBase::WorkerGlobalScopeBase [%p]", this)); + MOZ_ASSERT(mWorkerPrivate); +#ifdef DEBUG + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerThreadUsedOnlyForAssert = PR_GetCurrentThread(); +#endif + MOZ_ASSERT(mClientSource); + + MOZ_DIAGNOSTIC_ASSERT( + mSerialEventTarget, + "There should be an event target when a worker global is created."); + + // In workers, each DETH must have an owner. Because the global scope doesn't + // have one, let's set it as owner of itself. + BindToOwner(static_cast<nsIGlobalObject*>(this)); +} + +WorkerGlobalScopeBase::~WorkerGlobalScopeBase() = default; + +JSObject* WorkerGlobalScopeBase::GetGlobalJSObject() { + AssertIsOnWorkerThread(); + return GetWrapper(); +} + +JSObject* WorkerGlobalScopeBase::GetGlobalJSObjectPreserveColor() const { + AssertIsOnWorkerThread(); + return GetWrapperPreserveColor(); +} + +bool WorkerGlobalScopeBase::IsSharedMemoryAllowed() const { + AssertIsOnWorkerThread(); + return mWorkerPrivate->IsSharedMemoryAllowed(); +} + +bool WorkerGlobalScopeBase::ShouldResistFingerprinting( + RFPTarget aTarget) const { + AssertIsOnWorkerThread(); + return mWorkerPrivate->ShouldResistFingerprinting(aTarget); +} + +OriginTrials WorkerGlobalScopeBase::Trials() const { + AssertIsOnWorkerThread(); + return mWorkerPrivate->Trials(); +} + +StorageAccess WorkerGlobalScopeBase::GetStorageAccess() { + AssertIsOnWorkerThread(); + return mWorkerPrivate->StorageAccess(); +} + +Maybe<ClientInfo> WorkerGlobalScopeBase::GetClientInfo() const { + return Some(mClientSource->Info()); +} + +Maybe<ServiceWorkerDescriptor> WorkerGlobalScopeBase::GetController() const { + return mClientSource->GetController(); +} + +mozilla::Result<mozilla::ipc::PrincipalInfo, nsresult> +WorkerGlobalScopeBase::GetStorageKey() { + AssertIsOnWorkerThread(); + + const mozilla::ipc::PrincipalInfo& principalInfo = + mWorkerPrivate->GetEffectiveStoragePrincipalInfo(); + + // Block expanded and null principals, let content and system through. + if (principalInfo.type() != + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo && + principalInfo.type() != + mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo) { + return Err(NS_ERROR_DOM_SECURITY_ERR); + } + + return principalInfo; +} + +void WorkerGlobalScopeBase::Control( + const ServiceWorkerDescriptor& aServiceWorker) { + AssertIsOnWorkerThread(); + MOZ_DIAGNOSTIC_ASSERT(!mWorkerPrivate->IsChromeWorker()); + MOZ_DIAGNOSTIC_ASSERT(mWorkerPrivate->Kind() != WorkerKindService); + + if (IsBlobURI(mWorkerPrivate->GetBaseURI())) { + // Blob URL workers can only become controlled by inheriting from + // their parent. Make sure to note this properly. + mClientSource->InheritController(aServiceWorker); + } else { + // Otherwise this is a normal interception and we simply record the + // controller locally. + mClientSource->SetController(aServiceWorker); + } +} + +nsresult WorkerGlobalScopeBase::Dispatch( + already_AddRefed<nsIRunnable>&& aRunnable) const { + return SerialEventTarget()->Dispatch(std::move(aRunnable), + NS_DISPATCH_NORMAL); +} + +nsISerialEventTarget* WorkerGlobalScopeBase::SerialEventTarget() const { + AssertIsOnWorkerThread(); + return mSerialEventTarget; +} + +// See also AutoJSAPI::ReportException +void WorkerGlobalScopeBase::ReportError(JSContext* aCx, + JS::Handle<JS::Value> aError, + CallerType, ErrorResult& aRv) { + JS::ErrorReportBuilder jsReport(aCx); + JS::ExceptionStack exnStack(aCx, aError, nullptr); + if (!jsReport.init(aCx, exnStack, JS::ErrorReportBuilder::NoSideEffects)) { + return aRv.NoteJSContextException(aCx); + } + + // Before invoking ReportError, put the exception back on the context, + // because it may want to put it in its error events and has no other way + // to get hold of it. After we invoke ReportError, clear the exception on + // cx(), just in case ReportError didn't. + JS::SetPendingExceptionStack(aCx, exnStack); + mWorkerPrivate->ReportError(aCx, jsReport.toStringResult(), + jsReport.report()); + JS_ClearPendingException(aCx); +} + +void WorkerGlobalScopeBase::Atob(const nsAString& aAtob, nsAString& aOut, + ErrorResult& aRv) const { + AssertIsOnWorkerThread(); + aRv = nsContentUtils::Atob(aAtob, aOut); +} + +void WorkerGlobalScopeBase::Btoa(const nsAString& aBtoa, nsAString& aOut, + ErrorResult& aRv) const { + AssertIsOnWorkerThread(); + aRv = nsContentUtils::Btoa(aBtoa, aOut); +} + +already_AddRefed<Console> WorkerGlobalScopeBase::GetConsole(ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + if (!mConsole) { + mConsole = Console::Create(mWorkerPrivate->GetJSContext(), nullptr, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + RefPtr<Console> console = mConsole; + return console.forget(); +} + +uint64_t WorkerGlobalScopeBase::WindowID() const { + return mWorkerPrivate->WindowID(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(WorkerGlobalScope) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WorkerGlobalScope, + WorkerGlobalScopeBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCrypto) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPerformance) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWebTaskScheduler) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLocation) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNavigator) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFontFaceSet) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIndexedDB) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCacheStorage) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDebuggerNotificationManager) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WorkerGlobalScope, + WorkerGlobalScopeBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCrypto) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPerformance) + if (tmp->mWebTaskScheduler) { + tmp->mWebTaskScheduler->Disconnect(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWebTaskScheduler) + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocation) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNavigator) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIndexedDB) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCacheStorage) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDebuggerNotificationManager) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(WorkerGlobalScope, + WorkerGlobalScopeBase) + +WorkerGlobalScope::~WorkerGlobalScope() = default; + +void WorkerGlobalScope::NoteTerminating() { + LOG(("WorkerGlobalScope::NoteTerminating [%p]", this)); + if (IsDying()) { + return; + } + + StartDying(); +} + +void WorkerGlobalScope::NoteShuttingDown() { + MOZ_ASSERT(IsDying()); + LOG(("WorkerGlobalScope::NoteShuttingDown [%p]", this)); + + if (mNavigator) { + mNavigator->Invalidate(); + mNavigator = nullptr; + } +} + +Crypto* WorkerGlobalScope::GetCrypto(ErrorResult& aError) { + AssertIsOnWorkerThread(); + + if (!mCrypto) { + mCrypto = new Crypto(this); + } + + return mCrypto; +} + +already_AddRefed<CacheStorage> WorkerGlobalScope::GetCaches(ErrorResult& aRv) { + if (!mCacheStorage) { + mCacheStorage = CacheStorage::CreateOnWorker(cache::DEFAULT_NAMESPACE, this, + mWorkerPrivate, aRv); + } + + RefPtr<CacheStorage> ref = mCacheStorage; + return ref.forget(); +} + +bool WorkerGlobalScope::IsSecureContext() const { + bool globalSecure = JS::GetIsSecureContext( + js::GetNonCCWObjectRealm(GetWrapperPreserveColor())); + MOZ_ASSERT(globalSecure == mWorkerPrivate->IsSecureContext()); + return globalSecure; +} + +already_AddRefed<WorkerLocation> WorkerGlobalScope::Location() { + AssertIsOnWorkerThread(); + + if (!mLocation) { + mLocation = WorkerLocation::Create(mWorkerPrivate->GetLocationInfo()); + MOZ_ASSERT(mLocation); + } + + RefPtr<WorkerLocation> location = mLocation; + return location.forget(); +} + +already_AddRefed<WorkerNavigator> WorkerGlobalScope::Navigator() { + AssertIsOnWorkerThread(); + + if (!mNavigator) { + mNavigator = WorkerNavigator::Create(mWorkerPrivate->OnLine()); + MOZ_ASSERT(mNavigator); + } + + RefPtr<WorkerNavigator> navigator = mNavigator; + return navigator.forget(); +} + +already_AddRefed<WorkerNavigator> WorkerGlobalScope::GetExistingNavigator() + const { + AssertIsOnWorkerThread(); + + RefPtr<WorkerNavigator> navigator = mNavigator; + return navigator.forget(); +} + +FontFaceSet* WorkerGlobalScope::GetFonts(ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + if (!mFontFaceSet) { + mFontFaceSet = FontFaceSet::CreateForWorker(this, mWorkerPrivate); + if (MOZ_UNLIKELY(!mFontFaceSet)) { + aRv.ThrowInvalidStateError("Couldn't acquire worker reference"); + return nullptr; + } + } + + return mFontFaceSet; +} + +OnErrorEventHandlerNonNull* WorkerGlobalScope::GetOnerror() { + AssertIsOnWorkerThread(); + + EventListenerManager* elm = GetExistingListenerManager(); + return elm ? elm->GetOnErrorEventHandler() : nullptr; +} + +void WorkerGlobalScope::SetOnerror(OnErrorEventHandlerNonNull* aHandler) { + AssertIsOnWorkerThread(); + + EventListenerManager* elm = GetOrCreateListenerManager(); + if (elm) { + elm->SetEventHandler(aHandler); + } +} + +void WorkerGlobalScope::ImportScripts(JSContext* aCx, + const Sequence<nsString>& aScriptURLs, + ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + UniquePtr<SerializedStackHolder> stack; + if (mWorkerPrivate->IsWatchedByDevTools()) { + stack = GetCurrentStackForNetMonitor(aCx); + } + + { + AUTO_PROFILER_MARKER_TEXT( + "ImportScripts", JS, MarkerStack::Capture(), + profiler_thread_is_being_profiled_for_markers() + ? StringJoin(","_ns, aScriptURLs, + [](nsACString& dest, const auto& scriptUrl) { + AppendUTF16toUTF8( + Substring( + scriptUrl, 0, + std::min(size_t(128), scriptUrl.Length())), + dest); + }) + : nsAutoCString{}); + workerinternals::Load(mWorkerPrivate, std::move(stack), aScriptURLs, + WorkerScript, aRv); + } +} + +int32_t WorkerGlobalScope::SetTimeout(JSContext* aCx, Function& aHandler, + const int32_t aTimeout, + const Sequence<JS::Value>& aArguments, + ErrorResult& aRv) { + return SetTimeoutOrInterval(aCx, aHandler, aTimeout, aArguments, false, aRv); +} + +int32_t WorkerGlobalScope::SetTimeout(JSContext* aCx, const nsAString& aHandler, + const int32_t aTimeout, + const Sequence<JS::Value>& /* unused */, + ErrorResult& aRv) { + return SetTimeoutOrInterval(aCx, aHandler, aTimeout, false, aRv); +} + +void WorkerGlobalScope::ClearTimeout(int32_t aHandle) { + AssertIsOnWorkerThread(); + + DebuggerNotificationDispatch(this, DebuggerNotificationType::ClearTimeout); + + mWorkerPrivate->ClearTimeout(aHandle, Timeout::Reason::eTimeoutOrInterval); +} + +int32_t WorkerGlobalScope::SetInterval(JSContext* aCx, Function& aHandler, + const int32_t aTimeout, + const Sequence<JS::Value>& aArguments, + ErrorResult& aRv) { + return SetTimeoutOrInterval(aCx, aHandler, aTimeout, aArguments, true, aRv); +} + +int32_t WorkerGlobalScope::SetInterval(JSContext* aCx, + const nsAString& aHandler, + const int32_t aTimeout, + const Sequence<JS::Value>& /* unused */, + ErrorResult& aRv) { + return SetTimeoutOrInterval(aCx, aHandler, aTimeout, true, aRv); +} + +void WorkerGlobalScope::ClearInterval(int32_t aHandle) { + AssertIsOnWorkerThread(); + + DebuggerNotificationDispatch(this, DebuggerNotificationType::ClearInterval); + + mWorkerPrivate->ClearTimeout(aHandle, Timeout::Reason::eTimeoutOrInterval); +} + +int32_t WorkerGlobalScope::SetTimeoutOrInterval( + JSContext* aCx, Function& aHandler, const int32_t aTimeout, + const Sequence<JS::Value>& aArguments, bool aIsInterval, ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + DebuggerNotificationDispatch( + this, aIsInterval ? DebuggerNotificationType::SetInterval + : DebuggerNotificationType::SetTimeout); + + nsTArray<JS::Heap<JS::Value>> args; + if (!args.AppendElements(aArguments, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return 0; + } + + RefPtr<TimeoutHandler> handler = + new CallbackTimeoutHandler(aCx, this, &aHandler, std::move(args)); + + return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, aIsInterval, + Timeout::Reason::eTimeoutOrInterval, aRv); +} + +int32_t WorkerGlobalScope::SetTimeoutOrInterval(JSContext* aCx, + const nsAString& aHandler, + const int32_t aTimeout, + bool aIsInterval, + ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + DebuggerNotificationDispatch( + this, aIsInterval ? DebuggerNotificationType::SetInterval + : DebuggerNotificationType::SetTimeout); + + bool allowEval = false; + aRv = + CSPEvalChecker::CheckForWorker(aCx, mWorkerPrivate, aHandler, &allowEval); + if (NS_WARN_IF(aRv.Failed()) || !allowEval) { + return 0; + } + + RefPtr<TimeoutHandler> handler = + new WorkerScriptTimeoutHandler(aCx, this, aHandler); + + return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, aIsInterval, + Timeout::Reason::eTimeoutOrInterval, aRv); +} + +void WorkerGlobalScope::GetOrigin(nsAString& aOrigin) const { + AssertIsOnWorkerThread(); + nsContentUtils::GetWebExposedOriginSerialization( + mWorkerPrivate->GetPrincipal(), aOrigin); +} + +bool WorkerGlobalScope::CrossOriginIsolated() const { + return mWorkerPrivate->CrossOriginIsolated(); +} + +void WorkerGlobalScope::Dump(const Optional<nsAString>& aString) const { + AssertIsOnWorkerThread(); + + if (!aString.WasPassed()) { + return; + } + + if (!nsJSUtils::DumpEnabled()) { + return; + } + + NS_ConvertUTF16toUTF8 str(aString.Value()); + + MOZ_LOG(nsContentUtils::DOMDumpLog(), LogLevel::Debug, + ("[Worker.Dump] %s", str.get())); +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", str.get()); +#endif + fputs(str.get(), stdout); + fflush(stdout); +} + +Performance* WorkerGlobalScope::GetPerformance() { + AssertIsOnWorkerThread(); + + if (!mPerformance) { + mPerformance = Performance::CreateForWorker(this); + } + + return mPerformance; +} + +bool WorkerGlobalScope::IsInAutomation(JSContext* aCx, JSObject* /* unused */) { + return GetWorkerPrivateFromContext(aCx)->IsInAutomation(); +} + +void WorkerGlobalScope::GetJSTestingFunctions( + JSContext* aCx, JS::MutableHandle<JSObject*> aFunctions, ErrorResult& aRv) { + JSObject* obj = js::GetTestingFunctions(aCx); + if (!obj) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + aFunctions.set(obj); +} + +already_AddRefed<Promise> WorkerGlobalScope::Fetch( + const RequestOrUSVString& aInput, const RequestInit& aInit, + CallerType aCallerType, ErrorResult& aRv) { + return FetchRequest(this, aInput, aInit, aCallerType, aRv); +} + +already_AddRefed<IDBFactory> WorkerGlobalScope::GetIndexedDB( + JSContext* aCx, ErrorResult& aErrorResult) { + AssertIsOnWorkerThread(); + + RefPtr<IDBFactory> indexedDB = mIndexedDB; + + if (!indexedDB) { + StorageAccess access = mWorkerPrivate->StorageAccess(); + + if (access == StorageAccess::eDeny) { + NS_WARNING("IndexedDB is not allowed in this worker!"); + aErrorResult = NS_ERROR_DOM_SECURITY_ERR; + return nullptr; + } + + if (ShouldPartitionStorage(access) && + !StoragePartitioningEnabled(access, + mWorkerPrivate->CookieJarSettings())) { + NS_WARNING("IndexedDB is not allowed in this worker!"); + aErrorResult = NS_ERROR_DOM_SECURITY_ERR; + return nullptr; + } + + const PrincipalInfo& principalInfo = + mWorkerPrivate->GetEffectiveStoragePrincipalInfo(); + + auto res = IDBFactory::CreateForWorker(this, principalInfo, + mWorkerPrivate->WindowID()); + if (NS_WARN_IF(res.isErr())) { + aErrorResult = res.unwrapErr(); + return nullptr; + } + + indexedDB = res.unwrap(); + mIndexedDB = indexedDB; + } + + return indexedDB.forget(); +} + +WebTaskScheduler* WorkerGlobalScope::Scheduler() { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mWebTaskScheduler) { + mWebTaskScheduler = WebTaskScheduler::CreateForWorker(mWorkerPrivate); + } + + MOZ_ASSERT(mWebTaskScheduler); + return mWebTaskScheduler; +} + +WebTaskScheduler* WorkerGlobalScope::GetExistingScheduler() const { + return mWebTaskScheduler; +} + +already_AddRefed<Promise> WorkerGlobalScope::CreateImageBitmap( + const ImageBitmapSource& aImage, const ImageBitmapOptions& aOptions, + ErrorResult& aRv) { + return ImageBitmap::Create(this, aImage, Nothing(), aOptions, aRv); +} + +already_AddRefed<Promise> WorkerGlobalScope::CreateImageBitmap( + const ImageBitmapSource& aImage, int32_t aSx, int32_t aSy, int32_t aSw, + int32_t aSh, const ImageBitmapOptions& aOptions, ErrorResult& aRv) { + return ImageBitmap::Create( + this, aImage, Some(gfx::IntRect(aSx, aSy, aSw, aSh)), aOptions, aRv); +} + +// https://html.spec.whatwg.org/#structured-cloning +void WorkerGlobalScope::StructuredClone( + JSContext* aCx, JS::Handle<JS::Value> aValue, + const StructuredSerializeOptions& aOptions, + JS::MutableHandle<JS::Value> aRetval, ErrorResult& aError) { + nsContentUtils::StructuredClone(aCx, this, aValue, aOptions, aRetval, aError); +} + +mozilla::dom::DebuggerNotificationManager* +WorkerGlobalScope::GetOrCreateDebuggerNotificationManager() { + if (!mDebuggerNotificationManager) { + mDebuggerNotificationManager = new DebuggerNotificationManager(this); + } + + return mDebuggerNotificationManager; +} + +mozilla::dom::DebuggerNotificationManager* +WorkerGlobalScope::GetExistingDebuggerNotificationManager() { + return mDebuggerNotificationManager; +} + +Maybe<EventCallbackDebuggerNotificationType> +WorkerGlobalScope::GetDebuggerNotificationType() const { + return Some(EventCallbackDebuggerNotificationType::Global); +} + +RefPtr<ServiceWorkerRegistration> +WorkerGlobalScope::GetServiceWorkerRegistration( + const ServiceWorkerRegistrationDescriptor& aDescriptor) const { + AssertIsOnWorkerThread(); + RefPtr<ServiceWorkerRegistration> ref; + ForEachGlobalTeardownObserver( + [&](GlobalTeardownObserver* aObserver, bool* aDoneOut) { + RefPtr<ServiceWorkerRegistration> swr = do_QueryObject(aObserver); + if (!swr || !swr->MatchesDescriptor(aDescriptor)) { + return; + } + + ref = std::move(swr); + *aDoneOut = true; + }); + return ref; +} + +RefPtr<ServiceWorkerRegistration> +WorkerGlobalScope::GetOrCreateServiceWorkerRegistration( + const ServiceWorkerRegistrationDescriptor& aDescriptor) { + AssertIsOnWorkerThread(); + RefPtr<ServiceWorkerRegistration> ref = + GetServiceWorkerRegistration(aDescriptor); + if (!ref) { + ref = ServiceWorkerRegistration::CreateForWorker(mWorkerPrivate, this, + aDescriptor); + } + return ref; +} + +mozilla::dom::StorageManager* WorkerGlobalScope::GetStorageManager() { + return RefPtr(Navigator())->Storage(); +} + +// https://html.spec.whatwg.org/multipage/web-messaging.html#eligible-for-messaging +// * a WorkerGlobalScope object whose closing flag is false and whose worker +// is not a suspendable worker. +bool WorkerGlobalScope::IsEligibleForMessaging() { + return mIsEligibleForMessaging; +} +void WorkerGlobalScope::StorageAccessPermissionGranted() { + // Reset the IndexedDB factory. + mIndexedDB = nullptr; + + // Reset DOM Cache + mCacheStorage = nullptr; +} + +bool WorkerGlobalScope::WindowInteractionAllowed() const { + AssertIsOnWorkerThread(); + return mWindowInteractionsAllowed > 0; +} + +void WorkerGlobalScope::AllowWindowInteraction() { + AssertIsOnWorkerThread(); + mWindowInteractionsAllowed++; +} + +void WorkerGlobalScope::ConsumeWindowInteraction() { + AssertIsOnWorkerThread(); + MOZ_ASSERT(mWindowInteractionsAllowed); + mWindowInteractionsAllowed--; +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(DedicatedWorkerGlobalScope, + WorkerGlobalScope, mFrameRequestManager) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(DedicatedWorkerGlobalScope, + WorkerGlobalScope) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(DedicatedWorkerGlobalScope, + WorkerGlobalScope) + +DedicatedWorkerGlobalScope::DedicatedWorkerGlobalScope( + WorkerPrivate* aWorkerPrivate, UniquePtr<ClientSource> aClientSource, + const nsString& aName) + : WorkerGlobalScope(std::move(aWorkerPrivate), std::move(aClientSource)), + NamedWorkerGlobalScopeMixin(aName) {} + +bool DedicatedWorkerGlobalScope::WrapGlobalObject( + JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) { + AssertIsOnWorkerThread(); + MOZ_ASSERT(!mWorkerPrivate->IsSharedWorker()); + + JS::RealmOptions options; + mWorkerPrivate->CopyJSRealmOptions(options); + + xpc::SetPrefableRealmOptions(options); + + return DedicatedWorkerGlobalScope_Binding::Wrap( + aCx, this, this, options, + nsJSPrincipals::get(mWorkerPrivate->GetPrincipal()), aReflector); +} + +void DedicatedWorkerGlobalScope::PostMessage( + JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv) { + AssertIsOnWorkerThread(); + mWorkerPrivate->PostMessageToParent(aCx, aMessage, aTransferable, aRv); +} + +void DedicatedWorkerGlobalScope::PostMessage( + JSContext* aCx, JS::Handle<JS::Value> aMessage, + const StructuredSerializeOptions& aOptions, ErrorResult& aRv) { + AssertIsOnWorkerThread(); + mWorkerPrivate->PostMessageToParent(aCx, aMessage, aOptions.mTransfer, aRv); +} + +void DedicatedWorkerGlobalScope::Close() { + AssertIsOnWorkerThread(); + mWorkerPrivate->CloseInternal(); +} + +int32_t DedicatedWorkerGlobalScope::RequestAnimationFrame( + FrameRequestCallback& aCallback, ErrorResult& aError) { + AssertIsOnWorkerThread(); + + DebuggerNotificationDispatch(this, + DebuggerNotificationType::RequestAnimationFrame); + + // Ensure the worker is associated with a window. + if (mWorkerPrivate->WindowID() == UINT64_MAX) { + aError.ThrowNotSupportedError("Worker has no associated owner Window"); + return 0; + } + + if (!mVsyncChild) { + PBackgroundChild* bgChild = BackgroundChild::GetOrCreateForCurrentThread(); + mVsyncChild = MakeRefPtr<VsyncWorkerChild>(); + + if (!bgChild || !mVsyncChild->Initialize(mWorkerPrivate) || + !bgChild->SendPVsyncConstructor(mVsyncChild)) { + mVsyncChild->Destroy(); + mVsyncChild = nullptr; + aError.ThrowNotSupportedError( + "Worker failed to register for vsync to drive event loop"); + return 0; + } + } + + if (!mDocListener) { + mDocListener = WorkerDocumentListener::Create(mWorkerPrivate); + if (!mDocListener) { + aError.ThrowNotSupportedError( + "Worker failed to register for document visibility events"); + return 0; + } + } + + int32_t handle = 0; + aError = mFrameRequestManager.Schedule(aCallback, &handle); + if (!aError.Failed() && mDocumentVisible) { + mVsyncChild->TryObserve(); + } + return handle; +} + +void DedicatedWorkerGlobalScope::CancelAnimationFrame(int32_t aHandle, + ErrorResult& aError) { + AssertIsOnWorkerThread(); + + DebuggerNotificationDispatch(this, + DebuggerNotificationType::CancelAnimationFrame); + + // Ensure the worker is associated with a window. + if (mWorkerPrivate->WindowID() == UINT64_MAX) { + aError.ThrowNotSupportedError("Worker has no associated owner Window"); + return; + } + + mFrameRequestManager.Cancel(aHandle); + if (mVsyncChild && mFrameRequestManager.IsEmpty()) { + mVsyncChild->TryUnobserve(); + } +} + +void DedicatedWorkerGlobalScope::OnDocumentVisible(bool aVisible) { + AssertIsOnWorkerThread(); + + mDocumentVisible = aVisible; + + // We only change state immediately when we become visible. If we become + // hidden, then we wait for the next vsync tick to apply that. + if (aVisible && !mFrameRequestManager.IsEmpty()) { + mVsyncChild->TryObserve(); + } +} + +void DedicatedWorkerGlobalScope::OnVsync(const VsyncEvent& aVsync) { + AssertIsOnWorkerThread(); + + if (mFrameRequestManager.IsEmpty() || !mDocumentVisible) { + // If we ever receive a vsync event, and there are still no callbacks to + // process, or we remain hidden, we should disable observing them. By + // waiting an extra tick, we ensure we minimize extra IPC for content that + // does not call requestFrameAnimation directly during the callback, or + // that is rapidly toggling between hidden and visible. + mVsyncChild->TryUnobserve(); + return; + } + + nsTArray<FrameRequest> callbacks; + mFrameRequestManager.Take(callbacks); + + RefPtr<DedicatedWorkerGlobalScope> scope(this); + CallbackDebuggerNotificationGuard guard( + scope, DebuggerNotificationType::RequestAnimationFrameCallback); + + // This is similar to what we do in nsRefreshDriver::RunFrameRequestCallbacks + // and Performance::TimeStampToDOMHighResForRendering in order to have the + // same behaviour for requestAnimationFrame on both the main and worker + // threads. + DOMHighResTimeStamp timeStamp = 0; + if (!aVsync.mTime.IsNull()) { + timeStamp = mWorkerPrivate->TimeStampToDOMHighRes(aVsync.mTime); + // 0 is an inappropriate mixin for this this area; however CSS Animations + // needs to have it's Time Reduction Logic refactored, so it's currently + // only clamping for RFP mode. RFP mode gives a much lower time precision, + // so we accept the security leak here for now. + timeStamp = nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly( + timeStamp, 0, this->GetRTPCallerType()); + } + + for (auto& callback : callbacks) { + if (mFrameRequestManager.IsCanceled(callback.mHandle)) { + continue; + } + + // MOZ_KnownLive is OK, because the stack array `callbacks` keeps the + // callback alive and the mCallback strong reference can't be mutated by + // the call. + LogFrameRequestCallback::Run run(callback.mCallback); + MOZ_KnownLive(callback.mCallback)->Call(timeStamp); + } +} + +SharedWorkerGlobalScope::SharedWorkerGlobalScope( + WorkerPrivate* aWorkerPrivate, UniquePtr<ClientSource> aClientSource, + const nsString& aName) + : WorkerGlobalScope(std::move(aWorkerPrivate), std::move(aClientSource)), + NamedWorkerGlobalScopeMixin(aName) {} + +bool SharedWorkerGlobalScope::WrapGlobalObject( + JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) { + AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->IsSharedWorker()); + + JS::RealmOptions options; + mWorkerPrivate->CopyJSRealmOptions(options); + + return SharedWorkerGlobalScope_Binding::Wrap( + aCx, this, this, options, + nsJSPrincipals::get(mWorkerPrivate->GetPrincipal()), aReflector); +} + +void SharedWorkerGlobalScope::Close() { + AssertIsOnWorkerThread(); + mWorkerPrivate->CloseInternal(); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerGlobalScope, WorkerGlobalScope, + mClients, mExtensionBrowser, mRegistration) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerGlobalScope) +NS_INTERFACE_MAP_END_INHERITING(WorkerGlobalScope) + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerGlobalScope, WorkerGlobalScope) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerGlobalScope, WorkerGlobalScope) + +ServiceWorkerGlobalScope::ServiceWorkerGlobalScope( + WorkerPrivate* aWorkerPrivate, UniquePtr<ClientSource> aClientSource, + const ServiceWorkerRegistrationDescriptor& aRegistrationDescriptor) + : WorkerGlobalScope(std::move(aWorkerPrivate), std::move(aClientSource)), + mScope(NS_ConvertUTF8toUTF16(aRegistrationDescriptor.Scope())) + + // Eagerly create the registration because we will need to receive + // updates about the state of the registration. We can't wait until + // first access to start receiving these. + , + mRegistration( + GetOrCreateServiceWorkerRegistration(aRegistrationDescriptor)) {} + +ServiceWorkerGlobalScope::~ServiceWorkerGlobalScope() = default; + +bool ServiceWorkerGlobalScope::WrapGlobalObject( + JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) { + AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); + + JS::RealmOptions options; + mWorkerPrivate->CopyJSRealmOptions(options); + + return ServiceWorkerGlobalScope_Binding::Wrap( + aCx, this, this, options, + nsJSPrincipals::get(mWorkerPrivate->GetPrincipal()), aReflector); +} + +already_AddRefed<Clients> ServiceWorkerGlobalScope::GetClients() { + if (!mClients) { + mClients = new Clients(this); + } + + RefPtr<Clients> ref = mClients; + return ref.forget(); +} + +ServiceWorkerRegistration* ServiceWorkerGlobalScope::Registration() { + return mRegistration; +} + +EventHandlerNonNull* ServiceWorkerGlobalScope::GetOnfetch() { + AssertIsOnWorkerThread(); + + return GetEventHandler(nsGkAtoms::onfetch); +} + +namespace { + +class ReportFetchListenerWarningRunnable final : public Runnable { + const nsCString mScope; + nsString mSourceSpec; + uint32_t mLine; + uint32_t mColumn; + + public: + explicit ReportFetchListenerWarningRunnable(const nsString& aScope) + : mozilla::Runnable("ReportFetchListenerWarningRunnable"), + mScope(NS_ConvertUTF16toUTF8(aScope)) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + JSContext* cx = workerPrivate->GetJSContext(); + MOZ_ASSERT(cx); + + nsJSUtils::GetCallingLocation(cx, mSourceSpec, &mLine, &mColumn); + } + + NS_IMETHOD + Run() override { + AssertIsOnMainThread(); + + ServiceWorkerManager::LocalizeAndReportToAllClients( + mScope, "ServiceWorkerNoFetchHandler", nsTArray<nsString>{}, + nsIScriptError::warningFlag, mSourceSpec, u""_ns, mLine, mColumn); + + return NS_OK; + } +}; + +} // anonymous namespace + +void ServiceWorkerGlobalScope::NoteFetchHandlerWasAdded() const { + if (mWorkerPrivate->WorkerScriptExecutedSuccessfully()) { + RefPtr<Runnable> r = new ReportFetchListenerWarningRunnable(mScope); + mWorkerPrivate->DispatchToMainThreadForMessaging(r.forget()); + } + mWorkerPrivate->SetFetchHandlerWasAdded(); +} + +void ServiceWorkerGlobalScope::SetOnfetch( + mozilla::dom::EventHandlerNonNull* aCallback) { + AssertIsOnWorkerThread(); + + if (aCallback) { + NoteFetchHandlerWasAdded(); + } + SetEventHandler(nsGkAtoms::onfetch, aCallback); +} + +void ServiceWorkerGlobalScope::EventListenerAdded(nsAtom* aType) { + AssertIsOnWorkerThread(); + + if (aType == nsGkAtoms::onfetch) { + NoteFetchHandlerWasAdded(); + } +} + +already_AddRefed<Promise> ServiceWorkerGlobalScope::SkipWaiting( + ErrorResult& aRv) { + AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); + + RefPtr<Promise> promise = Promise::Create(this, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + using MozPromiseType = + decltype(mWorkerPrivate->SetServiceWorkerSkipWaitingFlag())::element_type; + auto holder = MakeRefPtr<DOMMozPromiseRequestHolder<MozPromiseType>>(this); + + mWorkerPrivate->SetServiceWorkerSkipWaitingFlag() + ->Then(GetCurrentSerialEventTarget(), __func__, + [holder, promise](const MozPromiseType::ResolveOrRejectValue&) { + holder->Complete(); + promise->MaybeResolveWithUndefined(); + }) + ->Track(*holder); + + return promise.forget(); +} + +SafeRefPtr<extensions::ExtensionBrowser> +ServiceWorkerGlobalScope::AcquireExtensionBrowser() { + if (!mExtensionBrowser) { + mExtensionBrowser = MakeSafeRefPtr<extensions::ExtensionBrowser>(this); + } + + return mExtensionBrowser.clonePtr(); +} + +bool WorkerDebuggerGlobalScope::WrapGlobalObject( + JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) { + AssertIsOnWorkerThread(); + + JS::RealmOptions options; + mWorkerPrivate->CopyJSRealmOptions(options); + + return WorkerDebuggerGlobalScope_Binding::Wrap( + aCx, this, this, options, + nsJSPrincipals::get(mWorkerPrivate->GetPrincipal()), aReflector); +} + +void WorkerDebuggerGlobalScope::GetGlobal(JSContext* aCx, + JS::MutableHandle<JSObject*> aGlobal, + ErrorResult& aRv) { + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (!scope) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + aGlobal.set(scope->GetWrapper()); +} + +void WorkerDebuggerGlobalScope::CreateSandbox( + JSContext* aCx, const nsAString& aName, JS::Handle<JSObject*> aPrototype, + JS::MutableHandle<JSObject*> aResult, ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + aResult.set(nullptr); + + JS::Rooted<JS::Value> protoVal(aCx); + protoVal.setObjectOrNull(aPrototype); + JS::Rooted<JSObject*> sandbox( + aCx, + SimpleGlobalObject::Create( + SimpleGlobalObject::GlobalType::WorkerDebuggerSandbox, protoVal)); + + if (!sandbox) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (!JS_WrapObject(aCx, &sandbox)) { + aRv.NoteJSContextException(aCx); + return; + } + + aResult.set(sandbox); +} + +void WorkerDebuggerGlobalScope::LoadSubScript( + JSContext* aCx, const nsAString& aURL, + const Optional<JS::Handle<JSObject*>>& aSandbox, ErrorResult& aRv) { + AssertIsOnWorkerThread(); + + Maybe<JSAutoRealm> ar; + if (aSandbox.WasPassed()) { + // We only care about worker debugger sandbox objects here, so + // CheckedUnwrapStatic is fine. + JS::Rooted<JSObject*> sandbox(aCx, + js::CheckedUnwrapStatic(aSandbox.Value())); + if (!sandbox || !IsWorkerDebuggerSandbox(sandbox)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + ar.emplace(aCx, sandbox); + } + + nsTArray<nsString> urls; + urls.AppendElement(aURL); + workerinternals::Load(mWorkerPrivate, nullptr, urls, DebuggerScript, aRv); +} + +void WorkerDebuggerGlobalScope::EnterEventLoop() { + // We're on the worker thread here, and WorkerPrivate's refcounting is + // non-threadsafe: you can only do it on the parent thread. What that + // means in practice is that we're relying on it being kept alive while + // we run. Hopefully. + MOZ_KnownLive(mWorkerPrivate)->EnterDebuggerEventLoop(); +} + +void WorkerDebuggerGlobalScope::LeaveEventLoop() { + mWorkerPrivate->LeaveDebuggerEventLoop(); +} + +void WorkerDebuggerGlobalScope::PostMessage(const nsAString& aMessage) { + mWorkerPrivate->PostMessageToDebugger(aMessage); +} + +void WorkerDebuggerGlobalScope::SetImmediate(Function& aHandler, + ErrorResult& aRv) { + mWorkerPrivate->SetDebuggerImmediate(aHandler, aRv); +} + +void WorkerDebuggerGlobalScope::ReportError(JSContext* aCx, + const nsAString& aMessage) { + JS::AutoFilename chars; + uint32_t lineno = 0; + JS::DescribeScriptedCaller(aCx, &chars, &lineno); + nsString filename(NS_ConvertUTF8toUTF16(chars.get())); + mWorkerPrivate->ReportErrorToDebugger(filename, lineno, aMessage); +} + +void WorkerDebuggerGlobalScope::RetrieveConsoleEvents( + JSContext* aCx, nsTArray<JS::Value>& aEvents, ErrorResult& aRv) { + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (!scope) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<Console> console = scope->GetConsole(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + console->RetrieveConsoleEvents(aCx, aEvents, aRv); +} + +void WorkerDebuggerGlobalScope::ClearConsoleEvents(JSContext* aCx, + ErrorResult& aRv) { + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (!scope) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<Console> console = scope->GetConsoleIfExists(); + if (console) { + console->ClearStorage(); + } +} + +void WorkerDebuggerGlobalScope::SetConsoleEventHandler(JSContext* aCx, + AnyCallback* aHandler, + ErrorResult& aRv) { + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (!scope) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<Console> console = scope->GetConsole(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + console->SetConsoleEventHandler(aHandler); +} + +void WorkerDebuggerGlobalScope::Dump(JSContext* aCx, + const Optional<nsAString>& aString) const { + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (scope) { + scope->Dump(aString); + } +} + +bool IsWorkerGlobal(JSObject* object) { + return IS_INSTANCE_OF(WorkerGlobalScope, object); +} + +bool IsWorkerDebuggerGlobal(JSObject* object) { + return IS_INSTANCE_OF(WorkerDebuggerGlobalScope, object); +} + +bool IsWorkerDebuggerSandbox(JSObject* object) { + return SimpleGlobalObject::SimpleGlobalType(object) == + SimpleGlobalObject::GlobalType::WorkerDebuggerSandbox; +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerScope.h b/dom/workers/WorkerScope.h new file mode 100644 index 0000000000..7e00f9b59b --- /dev/null +++ b/dom/workers/WorkerScope.h @@ -0,0 +1,557 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workerscope_h__ +#define mozilla_dom_workerscope_h__ + +#include "js/TypeDecls.h" +#include "js/loader/ModuleLoaderBase.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/AnimationFrameProvider.h" +#include "mozilla/dom/ImageBitmapBinding.h" +#include "mozilla/dom/ImageBitmapSource.h" +#include "mozilla/dom/PerformanceWorker.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" +#include "nsWeakReference.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +class nsAtom; +class nsISerialEventTarget; + +namespace mozilla { +class ErrorResult; +struct VsyncEvent; + +namespace extensions { + +class ExtensionBrowser; + +} // namespace extensions + +namespace dom { + +class AnyCallback; +enum class CallerType : uint32_t; +class ClientInfo; +class ClientSource; +class Clients; +class Console; +class Crypto; +class DOMString; +class DebuggerNotificationManager; +enum class EventCallbackDebuggerNotificationType : uint8_t; +class EventHandlerNonNull; +class FontFaceSet; +class Function; +class IDBFactory; +class OnErrorEventHandlerNonNull; +template <typename T> +class Optional; +class Performance; +class Promise; +class RequestOrUSVString; +template <typename T> +class Sequence; +class ServiceWorkerDescriptor; +class ServiceWorkerRegistration; +class ServiceWorkerRegistrationDescriptor; +struct StructuredSerializeOptions; +class WorkerDocumentListener; +class WorkerLocation; +class WorkerNavigator; +class WorkerPrivate; +class VsyncWorkerChild; +class WebTaskScheduler; +class WebTaskSchedulerWorker; +struct RequestInit; + +namespace cache { + +class CacheStorage; + +} // namespace cache + +class WorkerGlobalScopeBase : public DOMEventTargetHelper, + public nsSupportsWeakReference, + public nsIGlobalObject { + friend class WorkerPrivate; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WorkerGlobalScopeBase, + DOMEventTargetHelper) + + WorkerGlobalScopeBase(WorkerPrivate* aWorkerPrivate, + UniquePtr<ClientSource> aClientSource); + + virtual bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) = 0; + + // EventTarget implementation + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) final { + MOZ_CRASH("WrapObject not supported; use WrapGlobalObject."); + } + + // nsIGlobalObject implementation + JSObject* GetGlobalJSObject() final; + + JSObject* GetGlobalJSObjectPreserveColor() const final; + + bool IsSharedMemoryAllowed() const final; + + bool ShouldResistFingerprinting(RFPTarget aTarget) const final; + + OriginTrials Trials() const final; + + StorageAccess GetStorageAccess() final; + + Maybe<ClientInfo> GetClientInfo() const final; + + Maybe<ServiceWorkerDescriptor> GetController() const final; + + mozilla::Result<mozilla::ipc::PrincipalInfo, nsresult> GetStorageKey() final; + + virtual void Control(const ServiceWorkerDescriptor& aServiceWorker); + + // DispatcherTrait implementation + nsresult Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) const final; + nsISerialEventTarget* SerialEventTarget() const final; + + MOZ_CAN_RUN_SCRIPT + void ReportError(JSContext* aCx, JS::Handle<JS::Value> aError, + CallerType aCallerType, ErrorResult& aRv); + + // atob, btoa, and dump are declared (separately) by both WorkerGlobalScope + // and WorkerDebuggerGlobalScope WebIDL interfaces + void Atob(const nsAString& aAtob, nsAString& aOut, ErrorResult& aRv) const; + + void Btoa(const nsAString& aBtoa, nsAString& aOut, ErrorResult& aRv) const; + + already_AddRefed<Console> GetConsole(ErrorResult& aRv); + + Console* GetConsoleIfExists() const { return mConsole; } + + void InitModuleLoader(JS::loader::ModuleLoaderBase* aModuleLoader) { + if (!mModuleLoader) { + mModuleLoader = aModuleLoader; + } + } + + // The nullptr here is not used, but is required to make the override method + // have the same signature as other GetModuleLoader methods on globals. + JS::loader::ModuleLoaderBase* GetModuleLoader( + JSContext* aCx = nullptr) override { + return mModuleLoader; + }; + + uint64_t WindowID() const; + + // Usually global scope dies earlier than the WorkerPrivate, but if we see + // it leak at least we can tell it to not carry away a dead pointer. + void NoteWorkerTerminated() { mWorkerPrivate = nullptr; } + + ClientSource& MutableClientSourceRef() const { return *mClientSource; } + + // WorkerPrivate wants to be able to forbid script when its state machine + // demands it. + void WorkerPrivateSaysForbidScript() { StartForbiddingScript(); } + void WorkerPrivateSaysAllowScript() { StopForbiddingScript(); } + + protected: + ~WorkerGlobalScopeBase(); + + CheckedUnsafePtr<WorkerPrivate> mWorkerPrivate; + + void AssertIsOnWorkerThread() const { + MOZ_ASSERT(mWorkerThreadUsedOnlyForAssert == PR_GetCurrentThread()); + } + + private: + RefPtr<Console> mConsole; + RefPtr<JS::loader::ModuleLoaderBase> mModuleLoader; + const UniquePtr<ClientSource> mClientSource; + nsCOMPtr<nsISerialEventTarget> mSerialEventTarget; +#ifdef DEBUG + PRThread* mWorkerThreadUsedOnlyForAssert; +#endif +}; + +namespace workerinternals { + +class NamedWorkerGlobalScopeMixin { + public: + explicit NamedWorkerGlobalScopeMixin(const nsAString& aName) : mName(aName) {} + + void GetName(DOMString& aName) const; + + protected: + ~NamedWorkerGlobalScopeMixin() = default; + + private: + const nsString mName; +}; + +} // namespace workerinternals + +class WorkerGlobalScope : public WorkerGlobalScopeBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WorkerGlobalScope, + WorkerGlobalScopeBase) + + using WorkerGlobalScopeBase::WorkerGlobalScopeBase; + + void NoteTerminating(); + + void NoteShuttingDown(); + + // nsIGlobalObject implementation + RefPtr<ServiceWorkerRegistration> GetServiceWorkerRegistration( + const ServiceWorkerRegistrationDescriptor& aDescriptor) const final; + + RefPtr<ServiceWorkerRegistration> GetOrCreateServiceWorkerRegistration( + const ServiceWorkerRegistrationDescriptor& aDescriptor) final; + + DebuggerNotificationManager* GetOrCreateDebuggerNotificationManager() final; + + DebuggerNotificationManager* GetExistingDebuggerNotificationManager() final; + + Maybe<EventCallbackDebuggerNotificationType> GetDebuggerNotificationType() + const final; + + mozilla::dom::StorageManager* GetStorageManager() final; + + void SetIsNotEligibleForMessaging() { mIsEligibleForMessaging = false; } + + bool IsEligibleForMessaging() final; + + // WorkerGlobalScope WebIDL implementation + WorkerGlobalScope* Self() { return this; } + + already_AddRefed<WorkerLocation> Location(); + + already_AddRefed<WorkerNavigator> Navigator(); + + already_AddRefed<WorkerNavigator> GetExistingNavigator() const; + + FontFaceSet* GetFonts(ErrorResult&); + FontFaceSet* GetFonts() final { return GetFonts(IgnoreErrors()); } + + void ImportScripts(JSContext* aCx, const Sequence<nsString>& aScriptURLs, + ErrorResult& aRv); + + OnErrorEventHandlerNonNull* GetOnerror(); + + void SetOnerror(OnErrorEventHandlerNonNull* aHandler); + + IMPL_EVENT_HANDLER(languagechange) + IMPL_EVENT_HANDLER(offline) + IMPL_EVENT_HANDLER(online) + IMPL_EVENT_HANDLER(rejectionhandled) + IMPL_EVENT_HANDLER(unhandledrejection) + + void Dump(const Optional<nsAString>& aString) const; + + Performance* GetPerformance(); + + Performance* GetPerformanceIfExists() const { return mPerformance; } + + static bool IsInAutomation(JSContext* aCx, JSObject*); + + void GetJSTestingFunctions(JSContext* aCx, + JS::MutableHandle<JSObject*> aFunctions, + ErrorResult& aRv); + + // GlobalCrypto WebIDL implementation + Crypto* GetCrypto(ErrorResult& aError); + + // WindowOrWorkerGlobalScope WebIDL implementation + void GetOrigin(nsAString& aOrigin) const; + + bool CrossOriginIsolated() const final; + + MOZ_CAN_RUN_SCRIPT + int32_t SetTimeout(JSContext* aCx, Function& aHandler, int32_t aTimeout, + const Sequence<JS::Value>& aArguments, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + int32_t SetTimeout(JSContext* aCx, const nsAString& aHandler, + int32_t aTimeout, const Sequence<JS::Value>&, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + void ClearTimeout(int32_t aHandle); + + MOZ_CAN_RUN_SCRIPT + int32_t SetInterval(JSContext* aCx, Function& aHandler, int32_t aTimeout, + const Sequence<JS::Value>& aArguments, ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT + int32_t SetInterval(JSContext* aCx, const nsAString& aHandler, + int32_t aTimeout, const Sequence<JS::Value>&, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + void ClearInterval(int32_t aHandle); + + already_AddRefed<Promise> CreateImageBitmap( + const ImageBitmapSource& aImage, const ImageBitmapOptions& aOptions, + ErrorResult& aRv); + + already_AddRefed<Promise> CreateImageBitmap( + const ImageBitmapSource& aImage, int32_t aSx, int32_t aSy, int32_t aSw, + int32_t aSh, const ImageBitmapOptions& aOptions, ErrorResult& aRv); + + void StructuredClone(JSContext* aCx, JS::Handle<JS::Value> aValue, + const StructuredSerializeOptions& aOptions, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aError); + + already_AddRefed<Promise> Fetch(const RequestOrUSVString& aInput, + const RequestInit& aInit, + CallerType aCallerType, ErrorResult& aRv); + + bool IsSecureContext() const; + + already_AddRefed<IDBFactory> GetIndexedDB(JSContext* aCx, + ErrorResult& aErrorResult); + + already_AddRefed<cache::CacheStorage> GetCaches(ErrorResult& aRv); + + WebTaskScheduler* Scheduler(); + WebTaskScheduler* GetExistingScheduler() const; + + bool WindowInteractionAllowed() const; + + void AllowWindowInteraction(); + + void ConsumeWindowInteraction(); + + void StorageAccessPermissionGranted(); + + virtual void OnDocumentVisible(bool aVisible) {} + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual void OnVsync(const VsyncEvent& aVsync) {} + + protected: + ~WorkerGlobalScope(); + + private: + MOZ_CAN_RUN_SCRIPT + int32_t SetTimeoutOrInterval(JSContext* aCx, Function& aHandler, + int32_t aTimeout, + const Sequence<JS::Value>& aArguments, + bool aIsInterval, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + int32_t SetTimeoutOrInterval(JSContext* aCx, const nsAString& aHandler, + int32_t aTimeout, bool aIsInterval, + ErrorResult& aRv); + + RefPtr<Crypto> mCrypto; + RefPtr<WorkerLocation> mLocation; + RefPtr<WorkerNavigator> mNavigator; + RefPtr<FontFaceSet> mFontFaceSet; + RefPtr<Performance> mPerformance; + RefPtr<IDBFactory> mIndexedDB; + RefPtr<cache::CacheStorage> mCacheStorage; + RefPtr<DebuggerNotificationManager> mDebuggerNotificationManager; + RefPtr<WebTaskSchedulerWorker> mWebTaskScheduler; + uint32_t mWindowInteractionsAllowed = 0; + bool mIsEligibleForMessaging{true}; +}; + +class DedicatedWorkerGlobalScope final + : public WorkerGlobalScope, + public workerinternals::NamedWorkerGlobalScopeMixin { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + DedicatedWorkerGlobalScope, WorkerGlobalScope) + + DedicatedWorkerGlobalScope(WorkerPrivate* aWorkerPrivate, + UniquePtr<ClientSource> aClientSource, + const nsString& aName); + + bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv); + + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const StructuredSerializeOptions& aOptions, + ErrorResult& aRv); + + void Close(); + + MOZ_CAN_RUN_SCRIPT + int32_t RequestAnimationFrame(FrameRequestCallback& aCallback, + ErrorResult& aError); + + MOZ_CAN_RUN_SCRIPT + void CancelAnimationFrame(int32_t aHandle, ErrorResult& aError); + + void OnDocumentVisible(bool aVisible) override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void OnVsync(const VsyncEvent& aVsync) override; + + IMPL_EVENT_HANDLER(message) + IMPL_EVENT_HANDLER(messageerror) + IMPL_EVENT_HANDLER(rtctransform) + + private: + ~DedicatedWorkerGlobalScope() = default; + + FrameRequestManager mFrameRequestManager; + RefPtr<VsyncWorkerChild> mVsyncChild; + RefPtr<WorkerDocumentListener> mDocListener; + bool mDocumentVisible = false; +}; + +class SharedWorkerGlobalScope final + : public WorkerGlobalScope, + public workerinternals::NamedWorkerGlobalScopeMixin { + public: + SharedWorkerGlobalScope(WorkerPrivate* aWorkerPrivate, + UniquePtr<ClientSource> aClientSource, + const nsString& aName); + + bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + void Close(); + + IMPL_EVENT_HANDLER(connect) + + private: + ~SharedWorkerGlobalScope() = default; +}; + +class ServiceWorkerGlobalScope final : public WorkerGlobalScope { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerGlobalScope, + WorkerGlobalScope) + + ServiceWorkerGlobalScope( + WorkerPrivate* aWorkerPrivate, UniquePtr<ClientSource> aClientSource, + const ServiceWorkerRegistrationDescriptor& aRegistrationDescriptor); + + bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + already_AddRefed<Clients> GetClients(); + + ServiceWorkerRegistration* Registration(); + + already_AddRefed<Promise> SkipWaiting(ErrorResult& aRv); + + SafeRefPtr<extensions::ExtensionBrowser> AcquireExtensionBrowser(); + + IMPL_EVENT_HANDLER(install) + IMPL_EVENT_HANDLER(activate) + + EventHandlerNonNull* GetOnfetch(); + + void SetOnfetch(EventHandlerNonNull* aCallback); + + void EventListenerAdded(nsAtom* aType) override; + + IMPL_EVENT_HANDLER(message) + IMPL_EVENT_HANDLER(messageerror) + + IMPL_EVENT_HANDLER(notificationclick) + IMPL_EVENT_HANDLER(notificationclose) + + IMPL_EVENT_HANDLER(push) + IMPL_EVENT_HANDLER(pushsubscriptionchange) + + private: + ~ServiceWorkerGlobalScope(); + + void NoteFetchHandlerWasAdded() const; + + RefPtr<Clients> mClients; + const nsString mScope; + RefPtr<ServiceWorkerRegistration> mRegistration; + SafeRefPtr<extensions::ExtensionBrowser> mExtensionBrowser; +}; + +class WorkerDebuggerGlobalScope final : public WorkerGlobalScopeBase { + public: + using WorkerGlobalScopeBase::WorkerGlobalScopeBase; + + bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + void Control(const ServiceWorkerDescriptor& aServiceWorker) override { + MOZ_CRASH("Can't control debugger workers."); + } + + void GetGlobal(JSContext* aCx, JS::MutableHandle<JSObject*> aGlobal, + ErrorResult& aRv); + + void CreateSandbox(JSContext* aCx, const nsAString& aName, + JS::Handle<JSObject*> aPrototype, + JS::MutableHandle<JSObject*> aResult, ErrorResult& aRv); + + void LoadSubScript(JSContext* aCx, const nsAString& aUrl, + const Optional<JS::Handle<JSObject*>>& aSandbox, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void EnterEventLoop(); + + void LeaveEventLoop(); + + void PostMessage(const nsAString& aMessage); + + void SetImmediate(Function& aHandler, ErrorResult& aRv); + + void ReportError(JSContext* aCx, const nsAString& aMessage); + + void RetrieveConsoleEvents(JSContext* aCx, nsTArray<JS::Value>& aEvents, + ErrorResult& aRv); + + void ClearConsoleEvents(JSContext* aCx, ErrorResult& aRv); + + void SetConsoleEventHandler(JSContext* aCx, AnyCallback* aHandler, + ErrorResult& aRv); + + void Dump(JSContext* aCx, const Optional<nsAString>& aString) const; + + IMPL_EVENT_HANDLER(message) + IMPL_EVENT_HANDLER(messageerror) + + private: + ~WorkerDebuggerGlobalScope() = default; +}; + +} // namespace dom +} // namespace mozilla + +inline nsISupports* ToSupports(mozilla::dom::WorkerGlobalScope* aScope) { + return static_cast<mozilla::dom::EventTarget*>(aScope); +} + +#endif /* mozilla_dom_workerscope_h__ */ diff --git a/dom/workers/WorkerStatus.h b/dom/workers/WorkerStatus.h new file mode 100644 index 0000000000..696abc5d2c --- /dev/null +++ b/dom/workers/WorkerStatus.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerStatus_h +#define mozilla_dom_workers_WorkerStatus_h + +namespace mozilla::dom { + +/** + * Use this chart to help figure out behavior during each of the closing + * statuses. Details below. + * + * +========================================================+ + * | Closing Statuses | + * +=============+=============+=================+==========+ + * | status | clear queue | abort execution | notified | + * +=============+=============+=================+==========+ + * | Closing | yes | no | no | + * +-------------+-------------+-----------------+----------+ + * | Canceling | yes | yes | yes | + * +-------------+-------------+-----------------+----------+ + * | Killing | yes | yes | yes | + * +-------------+-------------+-----------------+----------+ + */ + +enum WorkerStatus { + // Not yet scheduled. + Pending = 0, + + // This status means that the worker is active. + Running, + + // Inner script called close() on the worker global scope. Setting this + // status causes the worker to clear its queue of events but does not abort + // the currently running script. WorkerRef objects are not going to be + // notified because the behavior of APIs/Components should not change during + // this status yet. + Closing, + + // Either the user navigated away from the owning page or the owning page fell + // out of bfcache. Setting this status causes the worker to abort immediately. + // Since the page has gone away the worker may not post any messages. + Canceling, + + // The application is shutting down. Setting this status causes the worker to + // abort immediately. + Killing, + + // The worker is effectively dead. + Dead +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_workers_WorkerStatus_h */ diff --git a/dom/workers/WorkerTestUtils.cpp b/dom/workers/WorkerTestUtils.cpp new file mode 100644 index 0000000000..81fa41b03c --- /dev/null +++ b/dom/workers/WorkerTestUtils.cpp @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerTestUtils.h" + +namespace mozilla::dom { + +uint32_t WorkerTestUtils::CurrentTimerNestingLevel(const GlobalObject& aGlobal, + ErrorResult& aErr) { + MOZ_ASSERT(!NS_IsMainThread()); + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + return worker->GetCurrentTimerNestingLevel(); +} + +} // namespace mozilla::dom diff --git a/dom/workers/WorkerTestUtils.h b/dom/workers/WorkerTestUtils.h new file mode 100644 index 0000000000..6668fdf1f7 --- /dev/null +++ b/dom/workers/WorkerTestUtils.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WorkerTestUtils__ +#define mozilla_dom_WorkerTestUtils__ + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +/** + * dom/webidl/WorkerTestUtils.webidl defines APIs to expose worker's internal + * status for glass-box testing. The APIs are only exposed to Workers with prefs + * dom.workers.testing.enabled. + * + * WorkerTestUtils is the implementation of dom/webidl/WorkerTestUtils.webidl + */ +class WorkerTestUtils final { + public: + /** + * Expose the worker's current timer nesting level. + * + * The worker's current timer nesting level means the executing timer + * handler's timer nesting level. When there is no executing timer handler, 0 + * should be returned by this API. The maximum timer nesting level is 5. + * + * https://html.spec.whatwg.org/#timer-initialisation-steps + */ + static uint32_t CurrentTimerNestingLevel(const GlobalObject&, + ErrorResult& aErr); +}; + +} // namespace dom +} // namespace mozilla +#endif diff --git a/dom/workers/WorkerThread.cpp b/dom/workers/WorkerThread.cpp new file mode 100644 index 0000000000..19cf9cb364 --- /dev/null +++ b/dom/workers/WorkerThread.cpp @@ -0,0 +1,367 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerThread.h" + +#include <utility> +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/EventQueue.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/NotNull.h" +#include "mozilla/ThreadEventQueue.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsICancelableRunnable.h" +#include "nsIEventTarget.h" +#include "nsIRunnable.h" +#include "nsIThreadInternal.h" +#include "nsString.h" +#include "prthread.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +// The C stack size. We use the same stack size on all platforms for +// consistency. +// +// Note: Our typical equation of 256 machine words works out to 2MB on 64-bit +// platforms. Since that works out to the size of a VM huge page, that can +// sometimes lead to an OS allocating an entire huge page for the stack at once. +// To avoid this, we subtract the size of 2 pages, to be safe. +const uint32_t kWorkerStackSize = 256 * sizeof(size_t) * 1024 - 8192; + +} // namespace + +WorkerThreadFriendKey::WorkerThreadFriendKey() { + MOZ_COUNT_CTOR(WorkerThreadFriendKey); +} + +WorkerThreadFriendKey::~WorkerThreadFriendKey() { + MOZ_COUNT_DTOR(WorkerThreadFriendKey); +} + +class WorkerThread::Observer final : public nsIThreadObserver { + WorkerPrivate* mWorkerPrivate; + + public: + explicit Observer(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + NS_DECL_THREADSAFE_ISUPPORTS + + private: + ~Observer() { mWorkerPrivate->AssertIsOnWorkerThread(); } + + NS_DECL_NSITHREADOBSERVER +}; + +WorkerThread::WorkerThread(ConstructorKey) + : nsThread( + MakeNotNull<ThreadEventQueue*>(MakeUnique<mozilla::EventQueue>()), + nsThread::NOT_MAIN_THREAD, {.stackSize = kWorkerStackSize}), + mLock("WorkerThread::mLock"), + mWorkerPrivateCondVar(mLock, "WorkerThread::mWorkerPrivateCondVar"), + mWorkerPrivate(nullptr), + mOtherThreadsDispatchingViaEventTarget(0) +#ifdef DEBUG + , + mAcceptingNonWorkerRunnables(true) +#endif +{ +} + +WorkerThread::~WorkerThread() { + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(!mOtherThreadsDispatchingViaEventTarget); + MOZ_ASSERT(mAcceptingNonWorkerRunnables); +} + +// static +SafeRefPtr<WorkerThread> WorkerThread::Create( + const WorkerThreadFriendKey& /* aKey */) { + SafeRefPtr<WorkerThread> thread = + MakeSafeRefPtr<WorkerThread>(ConstructorKey()); + if (NS_FAILED(thread->Init("DOM Worker"_ns))) { + NS_WARNING("Failed to create new thread!"); + return nullptr; + } + + return thread; +} + +void WorkerThread::SetWorker(const WorkerThreadFriendKey& /* aKey */, + WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(PR_GetCurrentThread() == mThread); + + if (aWorkerPrivate) { + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(mAcceptingNonWorkerRunnables); + + mWorkerPrivate = aWorkerPrivate; +#ifdef DEBUG + mAcceptingNonWorkerRunnables = false; +#endif + } + + mObserver = new Observer(aWorkerPrivate); + MOZ_ALWAYS_SUCCEEDS(AddObserver(mObserver)); + } else { + MOZ_ALWAYS_SUCCEEDS(RemoveObserver(mObserver)); + mObserver = nullptr; + + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(!mAcceptingNonWorkerRunnables); + // mOtherThreadsDispatchingViaEventTarget can still be non-zero here + // because WorkerThread::Dispatch isn't atomic so a thread initiating + // dispatch can have dispatched a runnable at this thread allowing us to + // begin shutdown before that thread gets a chance to decrement + // mOtherThreadsDispatchingViaEventTarget back to 0. So we need to wait + // for that. + while (mOtherThreadsDispatchingViaEventTarget) { + mWorkerPrivateCondVar.Wait(); + } + +#ifdef DEBUG + mAcceptingNonWorkerRunnables = true; +#endif + mWorkerPrivate = nullptr; + } + } +} + +nsresult WorkerThread::DispatchPrimaryRunnable( + const WorkerThreadFriendKey& /* aKey */, + already_AddRefed<nsIRunnable> aRunnable) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + +#ifdef DEBUG + MOZ_ASSERT(PR_GetCurrentThread() != mThread); + MOZ_ASSERT(runnable); + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(mAcceptingNonWorkerRunnables); + } +#endif + + nsresult rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult WorkerThread::DispatchAnyThread( + const WorkerThreadFriendKey& /* aKey */, + already_AddRefed<WorkerRunnable> aWorkerRunnable) { + // May be called on any thread! + +#ifdef DEBUG + { + const bool onWorkerThread = PR_GetCurrentThread() == mThread; + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(!mAcceptingNonWorkerRunnables); + + if (onWorkerThread) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + } + } +#endif + + nsCOMPtr<nsIRunnable> runnable(aWorkerRunnable); + + nsresult rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We don't need to notify the worker's condition variable here because we're + // being called from worker-controlled code and it will make sure to wake up + // the worker thread if needed. + + return NS_OK; +} + +NS_IMETHODIMP +WorkerThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +WorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags) { + // May be called on any thread! + nsCOMPtr<nsIRunnable> runnable(aRunnable); // in case we exit early + + // Workers only support asynchronous dispatch. + if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { + return NS_ERROR_UNEXPECTED; + } + + const bool onWorkerThread = PR_GetCurrentThread() == mThread; + + WorkerPrivate* workerPrivate = nullptr; + if (onWorkerThread) { + // No need to lock here because it is only modified on this thread. + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + workerPrivate = mWorkerPrivate; + } else { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mOtherThreadsDispatchingViaEventTarget < UINT32_MAX); + + if (mWorkerPrivate) { + workerPrivate = mWorkerPrivate; + + // Incrementing this counter will make the worker thread sleep if it + // somehow tries to unset mWorkerPrivate while we're using it. + mOtherThreadsDispatchingViaEventTarget++; + } + } + + nsresult rv; + if (runnable && onWorkerThread) { + RefPtr<WorkerRunnable> workerRunnable = + workerPrivate->MaybeWrapAsWorkerRunnable(runnable.forget()); + rv = nsThread::Dispatch(workerRunnable.forget(), NS_DISPATCH_NORMAL); + } else { + rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + } + + if (!onWorkerThread && workerPrivate) { + // We need to wake the worker thread if we're not already on the right + // thread and the dispatch succeeded. + if (NS_SUCCEEDED(rv)) { + MutexAutoLock workerLock(workerPrivate->mMutex); + + workerPrivate->mCondVar.Notify(); + } + + // Now unset our waiting flag. + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mOtherThreadsDispatchingViaEventTarget); + + if (!--mOtherThreadsDispatchingViaEventTarget) { + mWorkerPrivateCondVar.Notify(); + } + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerThread::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +uint32_t WorkerThread::RecursionDepth( + const WorkerThreadFriendKey& /* aKey */) const { + MOZ_ASSERT(PR_GetCurrentThread() == mThread); + + return mNestedEventLoopDepth; +} + +NS_IMETHODIMP +WorkerThread::HasPendingEvents(bool* aResult) { + MOZ_ASSERT(aResult); + const bool onWorkerThread = PR_GetCurrentThread() == mThread; + // If is on the worker thread, call nsThread::HasPendingEvents directly. + if (onWorkerThread) { + return nsThread::HasPendingEvents(aResult); + } + // Checking if is on the parent thread, otherwise, returns unexpected error. + { + MutexAutoLock lock(mLock); + // return directly if the mWorkerPrivate has not yet set or had already + // unset + if (!mWorkerPrivate) { + *aResult = false; + return NS_OK; + } + if (!mWorkerPrivate->IsOnParentThread()) { + *aResult = false; + return NS_ERROR_UNEXPECTED; + } + } + *aResult = mEvents->HasPendingEvent(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(WorkerThread::Observer, nsIThreadObserver) + +NS_IMETHODIMP +WorkerThread::Observer::OnDispatchedEvent() { + MOZ_CRASH("OnDispatchedEvent() should never be called!"); +} + +NS_IMETHODIMP +WorkerThread::Observer::OnProcessNextEvent(nsIThreadInternal* /* aThread */, + bool aMayWait) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + // If the PBackground child is not created yet, then we must permit + // blocking event processing to support + // BackgroundChild::GetOrCreateCreateForCurrentThread(). If this occurs + // then we are spinning on the event queue at the start of + // PrimaryWorkerRunnable::Run() and don't want to process the event in + // mWorkerPrivate yet. + if (aMayWait) { + MOZ_ASSERT(CycleCollectedJSContext::Get()->RecursionDepth() == 2); + MOZ_ASSERT(!BackgroundChild::GetForCurrentThread()); + return NS_OK; + } + + mWorkerPrivate->OnProcessNextEvent(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerThread::Observer::AfterProcessNextEvent(nsIThreadInternal* /* aThread */, + bool /* aEventWasProcessed */) { + mWorkerPrivate->AssertIsOnWorkerThread(); + + mWorkerPrivate->AfterProcessNextEvent(); + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/WorkerThread.h b/dom/workers/WorkerThread.h new file mode 100644 index 0000000000..06624f60c1 --- /dev/null +++ b/dom/workers/WorkerThread.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerThread_h__ +#define mozilla_dom_workers_WorkerThread_h__ + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/CondVar.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "nsISupports.h" +#include "nsThread.h" +#include "nscore.h" + +class nsIRunnable; + +namespace mozilla { +class Runnable; + +namespace dom { + +class WorkerRunnable; +class WorkerPrivate; +template <class> +class WorkerPrivateParent; + +namespace workerinternals { +class RuntimeService; +} + +// This class lets us restrict the public methods that can be called on +// WorkerThread to RuntimeService and WorkerPrivate without letting them gain +// full access to private methods (as would happen if they were simply friends). +class WorkerThreadFriendKey { + friend class workerinternals::RuntimeService; + friend class WorkerPrivate; + friend class WorkerPrivateParent<WorkerPrivate>; + + WorkerThreadFriendKey(); + ~WorkerThreadFriendKey(); +}; + +class WorkerThread final : public nsThread { + class Observer; + + Mutex mLock MOZ_UNANNOTATED; + CondVar mWorkerPrivateCondVar; + + // Protected by nsThread::mLock. + WorkerPrivate* mWorkerPrivate; + + // Only touched on the target thread. + RefPtr<Observer> mObserver; + + // Protected by nsThread::mLock and waited on with mWorkerPrivateCondVar. + uint32_t mOtherThreadsDispatchingViaEventTarget; + +#ifdef DEBUG + // Protected by nsThread::mLock. + bool mAcceptingNonWorkerRunnables; +#endif + + // Using this struct we restrict access to the constructor while still being + // able to use MakeSafeRefPtr. + struct ConstructorKey {}; + + public: + explicit WorkerThread(ConstructorKey); + + static SafeRefPtr<WorkerThread> Create(const WorkerThreadFriendKey& aKey); + + void SetWorker(const WorkerThreadFriendKey& aKey, + WorkerPrivate* aWorkerPrivate); + + nsresult DispatchPrimaryRunnable(const WorkerThreadFriendKey& aKey, + already_AddRefed<nsIRunnable> aRunnable); + + nsresult DispatchAnyThread(const WorkerThreadFriendKey& aKey, + already_AddRefed<WorkerRunnable> aWorkerRunnable); + + uint32_t RecursionDepth(const WorkerThreadFriendKey& aKey) const; + + // Override HasPendingEvents to allow HasPendingEvents could be accessed by + // the parent thread. WorkerPrivate::IsEligibleForCC calls this method on the + // parent thread to check if there is any pending events on the worker thread. + NS_IMETHOD HasPendingEvents(bool* aHasPendingEvents) override; + + NS_INLINE_DECL_REFCOUNTING_INHERITED(WorkerThread, nsThread) + + private: + ~WorkerThread(); + + // This should only be called by consumers that have an + // nsIEventTarget/nsIThread pointer. + NS_IMETHOD + Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) override; + + NS_IMETHOD + DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) override; + + NS_IMETHOD + DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_WorkerThread_h__ diff --git a/dom/workers/loader/CacheLoadHandler.cpp b/dom/workers/loader/CacheLoadHandler.cpp new file mode 100644 index 0000000000..16f992e837 --- /dev/null +++ b/dom/workers/loader/CacheLoadHandler.cpp @@ -0,0 +1,651 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CacheLoadHandler.h" +#include "ScriptResponseHeaderProcessor.h" // ScriptResponseHeaderProcessor +#include "WorkerLoadContext.h" // WorkerLoadContext + +#include "nsIPrincipal.h" + +#include "nsIThreadRetargetableRequest.h" +#include "nsIXPConnect.h" + +#include "jsapi.h" +#include "nsNetUtil.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Encoding.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/cache/CacheTypes.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ServiceWorkerBinding.h" // ServiceWorkerState +#include "mozilla/Result.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WorkerScope.h" + +#include "mozilla/dom/workerinternals/ScriptLoader.h" // WorkerScriptLoader + +namespace mozilla { +namespace dom { + +namespace workerinternals::loader { + +NS_IMPL_ISUPPORTS0(CacheCreator) + +NS_IMPL_ISUPPORTS(CacheLoadHandler, nsIStreamLoaderObserver) + +NS_IMPL_ISUPPORTS0(CachePromiseHandler) + +CachePromiseHandler::CachePromiseHandler( + WorkerScriptLoader* aLoader, ThreadSafeRequestHandle* aRequestHandle) + : mLoader(aLoader), mRequestHandle(aRequestHandle) { + AssertIsOnMainThread(); + MOZ_ASSERT(mLoader); +} + +void CachePromiseHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // May already have been canceled by CacheLoadHandler::Fail from + // CancelMainThread. + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::WritingToCache || + loadContext->mCacheStatus == WorkerLoadContext::Cancel); + MOZ_ASSERT_IF(loadContext->mCacheStatus == WorkerLoadContext::Cancel, + !loadContext->mCachePromise); + + if (loadContext->mCachePromise) { + loadContext->mCacheStatus = WorkerLoadContext::Cached; + loadContext->mCachePromise = nullptr; + mRequestHandle->MaybeExecuteFinishedScripts(); + } +} + +void CachePromiseHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // May already have been canceled by CacheLoadHandler::Fail from + // CancelMainThread. + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::WritingToCache || + loadContext->mCacheStatus == WorkerLoadContext::Cancel); + loadContext->mCacheStatus = WorkerLoadContext::Cancel; + + loadContext->mCachePromise = nullptr; + + // This will delete the cache object and will call LoadingFinished() with an + // error for each ongoing operation. + auto* cacheCreator = mRequestHandle->GetCacheCreator(); + if (cacheCreator) { + cacheCreator->DeleteCache(NS_ERROR_FAILURE); + } +} + +CacheCreator::CacheCreator(WorkerPrivate* aWorkerPrivate) + : mCacheName(aWorkerPrivate->ServiceWorkerCacheName()), + mOriginAttributes(aWorkerPrivate->GetOriginAttributes()) { + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); +} + +nsresult CacheCreator::CreateCacheStorage(nsIPrincipal* aPrincipal) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mCacheStorage); + MOZ_ASSERT(aPrincipal); + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> sandbox(cx); + nsresult rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The JSContext is not in a realm, so CreateSandbox returned an unwrapped + // global. + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + + mSandboxGlobalObject = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!mSandboxGlobalObject)) { + return NS_ERROR_FAILURE; + } + + // If we're in private browsing mode, don't even try to create the + // CacheStorage. Instead, just fail immediately to terminate the + // ServiceWorker load. + if (NS_WARN_IF(mOriginAttributes.mPrivateBrowsingId > 0)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Create a CacheStorage bypassing its trusted origin checks. The + // ServiceWorker has already performed its own checks before getting + // to this point. + ErrorResult error; + mCacheStorage = CacheStorage::CreateOnMainThread( + mozilla::dom::cache::CHROME_ONLY_NAMESPACE, mSandboxGlobalObject, + aPrincipal, true /* force trusted origin */, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + return NS_OK; +} + +nsresult CacheCreator::Load(nsIPrincipal* aPrincipal) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mLoaders.IsEmpty()); + + nsresult rv = CreateCacheStorage(aPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ErrorResult error; + MOZ_ASSERT(!mCacheName.IsEmpty()); + RefPtr<Promise> promise = mCacheStorage->Open(mCacheName, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + promise->AppendNativeHandler(this); + return NS_OK; +} + +void CacheCreator::FailLoaders(nsresult aRv) { + AssertIsOnMainThread(); + + // Fail() can call LoadingFinished() which may call ExecuteFinishedScripts() + // which sets mCacheCreator to null, so hold a ref. + RefPtr<CacheCreator> kungfuDeathGrip = this; + + for (uint32_t i = 0, len = mLoaders.Length(); i < len; ++i) { + mLoaders[i]->Fail(aRv); + } + + mLoaders.Clear(); +} + +void CacheCreator::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + FailLoaders(NS_ERROR_FAILURE); +} + +void CacheCreator::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + if (!aValue.isObject()) { + FailLoaders(NS_ERROR_FAILURE); + return; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + Cache* cache = nullptr; + nsresult rv = UNWRAP_OBJECT(Cache, &obj, cache); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailLoaders(NS_ERROR_FAILURE); + return; + } + + mCache = cache; + MOZ_DIAGNOSTIC_ASSERT(mCache); + + // If the worker is canceled, CancelMainThread() will have cleared the + // loaders via DeleteCache(). + for (uint32_t i = 0, len = mLoaders.Length(); i < len; ++i) { + mLoaders[i]->Load(cache); + } +} + +void CacheCreator::DeleteCache(nsresult aReason) { + AssertIsOnMainThread(); + + // This is called when the load is canceled which can occur before + // mCacheStorage is initialized. + if (mCacheStorage) { + // It's safe to do this while Cache::Match() and Cache::Put() calls are + // running. + RefPtr<Promise> promise = mCacheStorage->Delete(mCacheName, IgnoreErrors()); + + // We don't care to know the result of the promise object. + } + + // Always call this here to ensure the loaders array is cleared. + FailLoaders(NS_ERROR_FAILURE); +} + +CacheLoadHandler::CacheLoadHandler(ThreadSafeWorkerRef* aWorkerRef, + ThreadSafeRequestHandle* aRequestHandle, + bool aIsWorkerScript, + bool aOnlyExistingCachedResourcesAllowed, + WorkerScriptLoader* aLoader) + : mRequestHandle(aRequestHandle), + mLoader(aLoader), + mWorkerRef(aWorkerRef), + mIsWorkerScript(aIsWorkerScript), + mFailed(false), + mOnlyExistingCachedResourcesAllowed(aOnlyExistingCachedResourcesAllowed) { + MOZ_ASSERT(aWorkerRef); + MOZ_ASSERT(aWorkerRef->Private()->IsServiceWorker()); + mMainThreadEventTarget = aWorkerRef->Private()->MainThreadEventTarget(); + MOZ_ASSERT(mMainThreadEventTarget); + mBaseURI = mLoader->GetBaseURI(); + AssertIsOnMainThread(); + + // Worker scripts are always decoded as UTF-8 per spec. + mDecoder = MakeUnique<ScriptDecoder>(UTF_8_ENCODING, + ScriptDecoder::BOMHandling::Remove); +} + +void CacheLoadHandler::Fail(nsresult aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(NS_FAILED(aRv)); + + if (mFailed) { + return; + } + + mFailed = true; + + if (mPump) { + MOZ_ASSERT_IF(!mRequestHandle->IsEmpty(), + mRequestHandle->GetContext()->mCacheStatus == + WorkerLoadContext::ReadingFromCache); + mPump->Cancel(aRv); + mPump = nullptr; + } + if (mRequestHandle->IsEmpty()) { + return; + } + + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + loadContext->mCacheStatus = WorkerLoadContext::Cancel; + + if (loadContext->mCachePromise) { + loadContext->mCachePromise->MaybeReject(aRv); + } + + loadContext->mCachePromise = nullptr; + + mRequestHandle->LoadingFinished(aRv); +} + +void CacheLoadHandler::Load(Cache* aCache) { + AssertIsOnMainThread(); + MOZ_ASSERT(aCache); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), loadContext->mRequest->mURL, + nullptr, mBaseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + nsAutoCString spec; + rv = uri->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + MOZ_ASSERT(loadContext->mFullURL.IsEmpty()); + CopyUTF8toUTF16(spec, loadContext->mFullURL); + + mozilla::dom::RequestOrUSVString request; + request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + + mozilla::dom::CacheQueryOptions params; + + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + + ErrorResult error; + RefPtr<Promise> promise = aCache->Match(jsapi.cx(), request, params, error); + if (NS_WARN_IF(error.Failed())) { + Fail(error.StealNSResult()); + return; + } + + promise->AppendNativeHandler(this); +} + +void CacheLoadHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + + MOZ_ASSERT(mRequestHandle->GetContext()->mCacheStatus == + WorkerLoadContext::Uncached); + Fail(NS_ERROR_FAILURE); +} + +void CacheLoadHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // If we have already called 'Fail', we should not proceed. If we cancelled, + // we should similarily not proceed. + if (mFailed) { + return; + } + + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::Uncached); + + nsresult rv; + + // The ServiceWorkerScriptCache will store data for any scripts it + // it knows about. This is always at least the top level script. + // Depending on if a previous version of the service worker has + // been installed or not it may also know about importScripts(). We + // must handle loading and offlining new importScripts() here, however. + if (aValue.isUndefined()) { + // If this is the main script or we're not loading a new service worker + // then this is an error. This can happen for internal reasons, like + // storage was probably wiped without removing the service worker + // registration. It can also happen for exposed reasons like the + // service worker script calling importScripts() after install. + if (NS_WARN_IF(mIsWorkerScript || mOnlyExistingCachedResourcesAllowed)) { + Fail(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + loadContext->mCacheStatus = WorkerLoadContext::ToBeCached; + rv = mLoader->LoadScript(mRequestHandle); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + } + return; + } + + MOZ_ASSERT(aValue.isObject()); + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + mozilla::dom::Response* response = nullptr; + rv = UNWRAP_OBJECT(Response, &obj, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + InternalHeaders* headers = response->GetInternalHeaders(); + + headers->Get("content-security-policy"_ns, mCSPHeaderValue, IgnoreErrors()); + headers->Get("content-security-policy-report-only"_ns, + mCSPReportOnlyHeaderValue, IgnoreErrors()); + headers->Get("referrer-policy"_ns, mReferrerPolicyHeaderValue, + IgnoreErrors()); + + nsAutoCString coepHeader; + headers->Get("cross-origin-embedder-policy"_ns, coepHeader, IgnoreErrors()); + + nsILoadInfo::CrossOriginEmbedderPolicy coep = + NS_GetCrossOriginEmbedderPolicyFromHeader( + coepHeader, mWorkerRef->Private()->Trials().IsEnabled( + OriginTrial::CoepCredentialless)); + + rv = ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( + mWorkerRef->Private(), coep, loadContext->IsTopLevel()); + + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + nsCOMPtr<nsIInputStream> inputStream; + response->GetBody(getter_AddRefs(inputStream)); + mChannelInfo = response->GetChannelInfo(); + const UniquePtr<PrincipalInfo>& pInfo = response->GetPrincipalInfo(); + if (pInfo) { + mPrincipalInfo = mozilla::MakeUnique<PrincipalInfo>(*pInfo); + } + + if (!inputStream) { + loadContext->mCacheStatus = WorkerLoadContext::Cached; + + if (mRequestHandle->IsCancelled()) { + auto* cacheCreator = mRequestHandle->GetCacheCreator(); + if (cacheCreator) { + cacheCreator->DeleteCache(mRequestHandle->GetCancelResult()); + } + return; + } + + nsresult rv = DataReceivedFromCache( + (uint8_t*)"", 0, mChannelInfo, std::move(mPrincipalInfo), + mCSPHeaderValue, mCSPReportOnlyHeaderValue, mReferrerPolicyHeaderValue); + + mRequestHandle->OnStreamComplete(rv); + return; + } + + MOZ_ASSERT(!mPump); + rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream.forget(), + 0, /* default segsize */ + 0, /* default segcount */ + false, /* default closeWhenDone */ + mMainThreadEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + rv = mPump->AsyncRead(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + Fail(rv); + return; + } + + nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(mPump); + if (rr) { + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + RefPtr<TaskQueue> queue = + TaskQueue::Create(sts.forget(), "CacheLoadHandler STS Delivery Queue"); + rv = rr->RetargetDeliveryTo(queue); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the nsIInputStreamPump to a IO thread."); + } + } + + loadContext->mCacheStatus = WorkerLoadContext::ReadingFromCache; +} + +NS_IMETHODIMP +CacheLoadHandler::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aStringLen, + const uint8_t* aString) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return NS_OK; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + mPump = nullptr; + + if (NS_FAILED(aStatus)) { + MOZ_ASSERT(loadContext->mCacheStatus == + WorkerLoadContext::ReadingFromCache || + loadContext->mCacheStatus == WorkerLoadContext::Cancel); + Fail(aStatus); + return NS_OK; + } + + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::ReadingFromCache); + loadContext->mCacheStatus = WorkerLoadContext::Cached; + + MOZ_ASSERT(mPrincipalInfo); + + nsresult rv = DataReceivedFromCache( + aString, aStringLen, mChannelInfo, std::move(mPrincipalInfo), + mCSPHeaderValue, mCSPReportOnlyHeaderValue, mReferrerPolicyHeaderValue); + return mRequestHandle->OnStreamComplete(rv); +} + +nsresult CacheLoadHandler::DataReceivedFromCache( + const uint8_t* aString, uint32_t aStringLen, + const mozilla::dom::ChannelInfo& aChannelInfo, + UniquePtr<PrincipalInfo> aPrincipalInfo, const nsACString& aCSPHeaderValue, + const nsACString& aCSPReportOnlyHeaderValue, + const nsACString& aReferrerPolicyHeaderValue) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return NS_OK; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::Cached); + MOZ_ASSERT(loadContext->mRequest); + + auto responsePrincipalOrErr = PrincipalInfoToPrincipal(*aPrincipalInfo); + MOZ_DIAGNOSTIC_ASSERT(responsePrincipalOrErr.isOk()); + + nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + + nsCOMPtr<nsIPrincipal> responsePrincipal = responsePrincipalOrErr.unwrap(); + + loadContext->mMutedErrorFlag.emplace(!principal->Subsumes(responsePrincipal)); + + // May be null. + Document* parentDoc = mWorkerRef->Private()->GetDocument(); + + // Use the regular ScriptDecoder Decoder for this grunt work! Should be just + // fine because we're running on the main thread. + nsresult rv; + + // Set the Source type to "text" for decoding. + loadContext->mRequest->SetTextSource(loadContext); + + rv = mDecoder->DecodeRawData(loadContext->mRequest, aString, aStringLen, + /* aEndOfStream = */ true); + NS_ENSURE_SUCCESS(rv, rv); + + if (!loadContext->mRequest->ScriptTextLength()) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + parentDoc, nsContentUtils::eDOM_PROPERTIES, + "EmptyWorkerSourceWarning"); + } + + nsCOMPtr<nsIURI> finalURI; + rv = NS_NewURI(getter_AddRefs(finalURI), loadContext->mFullURL); + if (!loadContext->mRequest->mBaseURL) { + loadContext->mRequest->mBaseURL = finalURI; + } + if (loadContext->IsTopLevel()) { + if (NS_SUCCEEDED(rv)) { + mWorkerRef->Private()->SetBaseURI(finalURI); + } + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); + MOZ_DIAGNOSTIC_ASSERT(principal); + + bool equal = false; + MOZ_ALWAYS_SUCCEEDS(responsePrincipal->Equals(principal, &equal)); + MOZ_DIAGNOSTIC_ASSERT(equal); + + nsCOMPtr<nsIContentSecurityPolicy> csp; + if (parentDoc) { + csp = parentDoc->GetCsp(); + } + MOZ_DIAGNOSTIC_ASSERT(!csp); +#endif + + mWorkerRef->Private()->InitChannelInfo(aChannelInfo); + + nsILoadGroup* loadGroup = mWorkerRef->Private()->GetLoadGroup(); + MOZ_DIAGNOSTIC_ASSERT(loadGroup); + + // Override the principal on the WorkerPrivate. This is only necessary + // in order to get a principal with exactly the correct URL. The fetch + // referrer logic depends on the WorkerPrivate principal having a URL + // that matches the worker script URL. If bug 1340694 is ever fixed + // this can be removed. + // XXX: force the partitionedPrincipal to be equal to the response one. + // This is OK for now because we don't want to expose partitionedPrincipal + // functionality in ServiceWorkers yet. + rv = mWorkerRef->Private()->SetPrincipalsAndCSPOnMainThread( + responsePrincipal, responsePrincipal, loadGroup, nullptr); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + rv = mWorkerRef->Private()->SetCSPFromHeaderValues( + aCSPHeaderValue, aCSPReportOnlyHeaderValue); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + mWorkerRef->Private()->UpdateReferrerInfoFromHeader( + aReferrerPolicyHeaderValue); + } + + if (NS_SUCCEEDED(rv)) { + DataReceived(); + } + + return rv; +} + +void CacheLoadHandler::DataReceived() { + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + if (loadContext->IsTopLevel()) { + WorkerPrivate* parent = mWorkerRef->Private()->GetParent(); + + if (parent) { + // XHR Params Allowed + mWorkerRef->Private()->SetXHRParamsAllowed(parent->XHRParamsAllowed()); + + // Set Eval and ContentSecurityPolicy + mWorkerRef->Private()->SetCsp(parent->GetCsp()); + mWorkerRef->Private()->SetEvalAllowed(parent->IsEvalAllowed()); + mWorkerRef->Private()->SetWasmEvalAllowed(parent->IsWasmEvalAllowed()); + } + } +} + +} // namespace workerinternals::loader + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/CacheLoadHandler.h b/dom/workers/loader/CacheLoadHandler.h new file mode 100644 index 0000000000..b1e164b79e --- /dev/null +++ b/dom/workers/loader/CacheLoadHandler.h @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_CacheLoadHandler_h__ +#define mozilla_dom_workers_CacheLoadHandler_h__ + +#include "nsIContentPolicy.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamLoader.h" +#include "nsStringFwd.h" +#include "nsStreamUtils.h" + +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/ScriptLoadHandler.h" +#include "mozilla/dom/cache/Cache.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" + +#include "mozilla/dom/workerinternals/ScriptLoader.h" + +using mozilla::dom::cache::Cache; +using mozilla::dom::cache::CacheStorage; +using mozilla::ipc::PrincipalInfo; + +namespace mozilla::dom { + +class WorkerLoadContext; + +namespace workerinternals::loader { + +/* + * [DOMDOC] CacheLoadHandler for Workers + * + * A LoadHandler is a ScriptLoader helper class that reacts to an + * nsIStreamLoader's events for loading JS scripts. It is primarily responsible + * for decoding the stream into UTF8 or UTF16. Additionally, it takes care of + * any work that needs to follow the completion of a stream. Every LoadHandler + * also manages additional tasks for the type of load that it is doing. + * + * CacheLoadHandler is a specialized LoadHandler used by ServiceWorkers to + * implement the installation model used by ServiceWorkers to support running + * offline. When a ServiceWorker is installed, its main script is evaluated and + * all script resources that are loaded are saved. The spec does not specify the + * storage mechanism for this, but we chose to reuse the Cache API[1] mechanism + * that we expose to content to also store the script and its dependencies. We + * store the script resources in a special chrome namespace CacheStorage that is + * not visible to content. Each distinct ServiceWorker installation gets its own + * Cache keyed by a randomly-generated UUID. + * + * In terms of specification, this class implements step 4 of + * https://w3c.github.io/ServiceWorker/#importscripts + * + * Relationship to NetworkLoadHandler + * + * During ServiceWorker installation, the CacheLoadHandler falls back on the + * NetworkLoadHandler by calling `mLoader->LoadScript(...)`. If a script has not + * been seen before, then we will fall back on loading from the network. + * However, if the ServiceWorker is already installed, an error will be + * generated and the ServiceWorker will fail to load, per spec. + * + * CacheLoadHandler does not persist some pieces of information, such as the + * sourceMapUrl. Also, the DOM Cache API storage does not yet support alternate + * data streams for JS Bytecode or WASM caching; this is tracked by Bug 1336199. + * + * [1]: https://developer.mozilla.org/en-US/docs/Web/API/caches + * + */ + +class CacheLoadHandler final : public PromiseNativeHandler, + public nsIStreamLoaderObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + CacheLoadHandler(ThreadSafeWorkerRef* aWorkerRef, + ThreadSafeRequestHandle* aRequestHandle, + bool aIsWorkerScript, + bool aOnlyExistingCachedResourcesAllowed, + WorkerScriptLoader* aLoader); + + void Fail(nsresult aRv); + + void Load(Cache* aCache); + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + private: + ~CacheLoadHandler() { AssertIsOnMainThread(); } + + nsresult DataReceivedFromCache(const uint8_t* aString, uint32_t aStringLen, + const mozilla::dom::ChannelInfo& aChannelInfo, + UniquePtr<PrincipalInfo> aPrincipalInfo, + const nsACString& aCSPHeaderValue, + const nsACString& aCSPReportOnlyHeaderValue, + const nsACString& aReferrerPolicyHeaderValue); + void DataReceived(); + + RefPtr<ThreadSafeRequestHandle> mRequestHandle; + const RefPtr<WorkerScriptLoader> mLoader; + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + const bool mIsWorkerScript; + bool mFailed; + bool mOnlyExistingCachedResourcesAllowed; + nsCOMPtr<nsIInputStreamPump> mPump; + nsCOMPtr<nsIURI> mBaseURI; + mozilla::dom::ChannelInfo mChannelInfo; + UniquePtr<PrincipalInfo> mPrincipalInfo; + UniquePtr<ScriptDecoder> mDecoder; + nsCString mCSPHeaderValue; + nsCString mCSPReportOnlyHeaderValue; + nsCString mReferrerPolicyHeaderValue; + nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget; +}; + +/* + * CacheCreator + * + * The CacheCreator is responsible for maintaining a CacheStorage for the + * purposes of caching ServiceWorkers (see comment on CacheLoadHandler). In + * addition, it tracks all CacheLoadHandlers and is used for cleanup once + * loading has finished. + * + */ + +class CacheCreator final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + explicit CacheCreator(WorkerPrivate* aWorkerPrivate); + + void AddLoader(MovingNotNull<RefPtr<CacheLoadHandler>> aLoader) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mCacheStorage); + mLoaders.AppendElement(std::move(aLoader)); + } + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + // Try to load from cache with aPrincipal used for cache access. + nsresult Load(nsIPrincipal* aPrincipal); + + Cache* Cache_() const { + AssertIsOnMainThread(); + MOZ_ASSERT(mCache); + return mCache; + } + + nsIGlobalObject* Global() const { + AssertIsOnMainThread(); + MOZ_ASSERT(mSandboxGlobalObject); + return mSandboxGlobalObject; + } + + void DeleteCache(nsresult aReason); + + private: + ~CacheCreator() = default; + + nsresult CreateCacheStorage(nsIPrincipal* aPrincipal); + + void FailLoaders(nsresult aRv); + + RefPtr<Cache> mCache; + RefPtr<CacheStorage> mCacheStorage; + nsCOMPtr<nsIGlobalObject> mSandboxGlobalObject; + nsTArray<NotNull<RefPtr<CacheLoadHandler>>> mLoaders; + + nsString mCacheName; + OriginAttributes mOriginAttributes; +}; + +/* + * CachePromiseHandler + * + * This promise handler is used to track if a ServiceWorker has been written to + * Cache. It is responsible for tracking the state of the ServiceWorker being + * cached. It also handles cancelling caching of a ServiceWorker if loading is + * interrupted. It is initialized by the NetworkLoadHandler as part of the first + * load of a ServiceWorker. + * + */ +class CachePromiseHandler final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + CachePromiseHandler(WorkerScriptLoader* aLoader, + ThreadSafeRequestHandle* aRequestHandle); + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + private: + ~CachePromiseHandler() { AssertIsOnMainThread(); } + + RefPtr<WorkerScriptLoader> mLoader; + RefPtr<ThreadSafeRequestHandle> mRequestHandle; +}; + +} // namespace workerinternals::loader +} // namespace mozilla::dom + +#endif /* mozilla_dom_workers_CacheLoadHandler_h__ */ diff --git a/dom/workers/loader/NetworkLoadHandler.cpp b/dom/workers/loader/NetworkLoadHandler.cpp new file mode 100644 index 0000000000..9c2c243066 --- /dev/null +++ b/dom/workers/loader/NetworkLoadHandler.cpp @@ -0,0 +1,393 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "NetworkLoadHandler.h" +#include "CacheLoadHandler.h" // CachePromiseHandler + +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIPrincipal.h" +#include "nsIScriptError.h" +#include "nsNetUtil.h" + +#include "mozilla/Encoding.h" +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/WorkerScope.h" + +#include "mozilla/dom/workerinternals/ScriptLoader.h" // WorkerScriptLoader + +using mozilla::ipc::PrincipalInfo; + +namespace mozilla { +namespace dom { + +namespace workerinternals::loader { + +NS_IMPL_ISUPPORTS(NetworkLoadHandler, nsIStreamLoaderObserver, + nsIRequestObserver) + +NetworkLoadHandler::NetworkLoadHandler(WorkerScriptLoader* aLoader, + ThreadSafeRequestHandle* aRequestHandle) + : mLoader(aLoader), + mWorkerRef(aLoader->mWorkerRef), + mRequestHandle(aRequestHandle) { + MOZ_ASSERT(mLoader); + + // Worker scripts are always decoded as UTF-8 per spec. + mDecoder = MakeUnique<ScriptDecoder>(UTF_8_ENCODING, + ScriptDecoder::BOMHandling::Remove); +} + +NS_IMETHODIMP +NetworkLoadHandler::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aStringLen, + const uint8_t* aString) { + // If we have cancelled, or we have no mRequest, it means that the loader has + // shut down and we can exit early. If the cancel result is still NS_OK + if (mRequestHandle->IsEmpty()) { + return NS_OK; + } + nsresult rv = DataReceivedFromNetwork(aLoader, aStatus, aStringLen, aString); + return mRequestHandle->OnStreamComplete(rv); +} + +nsresult NetworkLoadHandler::DataReceivedFromNetwork(nsIStreamLoader* aLoader, + nsresult aStatus, + uint32_t aStringLen, + const uint8_t* aString) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + if (!loadContext->mChannel) { + return NS_BINDING_ABORTED; + } + + loadContext->mChannel = nullptr; + + if (NS_FAILED(aStatus)) { + return aStatus; + } + + if (mRequestHandle->IsCancelled()) { + return mRequestHandle->GetCancelResult(); + } + + NS_ASSERTION(aString, "This should never be null!"); + + nsCOMPtr<nsIRequest> request; + nsresult rv = aLoader->GetRequest(getter_AddRefs(request)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + MOZ_ASSERT(channel); + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(ssm, "Should never be null!"); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + rv = + ssm->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + +#ifdef DEBUG + if (loadContext->IsTopLevel()) { + nsCOMPtr<nsIPrincipal> loadingPrincipal = + mWorkerRef->Private()->GetLoadingPrincipal(); + // if we are not in a ServiceWorker, and the principal is not null, then + // the loading principal must subsume the worker principal if it is not a + // nullPrincipal (sandbox). + MOZ_ASSERT(!loadingPrincipal || loadingPrincipal->GetIsNullPrincipal() || + principal->GetIsNullPrincipal() || + loadingPrincipal->Subsumes(principal)); + } +#endif + + // We don't mute the main worker script becase we've already done + // same-origin checks on them so we should be able to see their errors. + // Note that for data: url, where we allow it through the same-origin check + // but then give it a different origin. + loadContext->mMutedErrorFlag.emplace(!loadContext->IsTopLevel() && + !principal->Subsumes(channelPrincipal)); + + // Make sure we're not seeing the result of a 404 or something by checking + // the 'requestSucceeded' attribute on the http channel. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request); + nsAutoCString tCspHeaderValue, tCspROHeaderValue, tRPHeaderCValue; + + if (httpChannel) { + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + NS_ENSURE_SUCCESS(rv, rv); + + if (!requestSucceeded) { + return NS_ERROR_NOT_AVAILABLE; + } + + Unused << httpChannel->GetResponseHeader("content-security-policy"_ns, + tCspHeaderValue); + + Unused << httpChannel->GetResponseHeader( + "content-security-policy-report-only"_ns, tCspROHeaderValue); + + Unused << httpChannel->GetResponseHeader("referrer-policy"_ns, + tRPHeaderCValue); + + nsAutoCString sourceMapURL; + if (nsContentUtils::GetSourceMapURL(httpChannel, sourceMapURL)) { + loadContext->mRequest->mSourceMapURL = + Some(NS_ConvertUTF8toUTF16(sourceMapURL)); + } + } + + // May be null. + Document* parentDoc = mWorkerRef->Private()->GetDocument(); + + // Set the Source type to "text" for decoding. + loadContext->mRequest->SetTextSource(loadContext); + + // Use the regular ScriptDecoder Decoder for this grunt work! Should be just + // fine because we're running on the main thread. + rv = mDecoder->DecodeRawData(loadContext->mRequest, aString, aStringLen, + /* aEndOfStream = */ true); + NS_ENSURE_SUCCESS(rv, rv); + + if (!loadContext->mRequest->ScriptTextLength()) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + parentDoc, nsContentUtils::eDOM_PROPERTIES, + "EmptyWorkerSourceWarning"); + } + + // For modules, we need to store the base URI on the module request object, + // rather than on the worker private (as we do for classic scripts). This is + // because module loading is shared across multiple components, with + // ScriptLoadRequests being the common structure among them. This specific + // use of the base url is used when resolving the module specifier for child + // modules. + nsCOMPtr<nsIURI> uri; + rv = channel->GetOriginalURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + loadContext->mRequest->SetBaseURLFromChannelAndOriginalURI(channel, uri); + + // Figure out what we actually loaded. + nsCOMPtr<nsIURI> finalURI; + rv = NS_GetFinalChannelURI(channel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + if (principal->IsSameOrigin(finalURI)) { + nsCString filename; + rv = finalURI->GetSpec(filename); + NS_ENSURE_SUCCESS(rv, rv); + + if (!filename.IsEmpty()) { + // This will help callers figure out what their script url resolved to + // in case of errors, and is used for debugging. + // The full URL shouldn't be exposed to the debugger if cross origin. + // See Bug 1634872. + loadContext->mRequest->mURL = filename; + } + } + + // Update the principal of the worker and its base URI if we just loaded the + // worker's primary script. + bool isDynamic = loadContext->mRequest->IsModuleRequest() && + loadContext->mRequest->AsModuleRequest()->IsDynamicImport(); + if (loadContext->IsTopLevel() && !isDynamic) { + // Take care of the base URI first. + mWorkerRef->Private()->SetBaseURI(finalURI); + + // Store the channel info if needed. + mWorkerRef->Private()->InitChannelInfo(channel); + + // Our final channel principal should match the loading principal + // in terms of the origin. This used to be an assert, but it seems + // there are some rare cases where this check can fail in practice. + // Perhaps some browser script setting nsIChannel.owner, etc. + NS_ENSURE_TRUE(mWorkerRef->Private()->FinalChannelPrincipalIsValid(channel), + NS_ERROR_FAILURE); + + // However, we must still override the principal since the nsIPrincipal + // URL may be different due to same-origin redirects. Unfortunately this + // URL must exactly match the final worker script URL in order to + // properly set the referrer header on fetch/xhr requests. If bug 1340694 + // is ever fixed this can be removed. + rv = mWorkerRef->Private()->SetPrincipalsAndCSPFromChannel(channel); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIContentSecurityPolicy> csp = mWorkerRef->Private()->GetCsp(); + // We did inherit CSP in bug 1223647. If we do not already have a CSP, we + // should get it from the HTTP headers on the worker script. + if (!csp) { + rv = mWorkerRef->Private()->SetCSPFromHeaderValues(tCspHeaderValue, + tCspROHeaderValue); + NS_ENSURE_SUCCESS(rv, rv); + } else { + csp->EnsureEventTarget(mWorkerRef->Private()->MainThreadEventTarget()); + } + + mWorkerRef->Private()->UpdateReferrerInfoFromHeader(tRPHeaderCValue); + + WorkerPrivate* parent = mWorkerRef->Private()->GetParent(); + if (parent) { + // XHR Params Allowed + mWorkerRef->Private()->SetXHRParamsAllowed(parent->XHRParamsAllowed()); + } + + nsCOMPtr<nsILoadInfo> chanLoadInfo = channel->LoadInfo(); + if (chanLoadInfo) { + mLoader->SetController(chanLoadInfo->GetController()); + } + + // If we are loading a blob URL we must inherit the controller + // from the parent. This is a bit odd as the blob URL may have + // been created in a different context with a different controller. + // For now, though, this is what the spec says. See: + // + // https://github.com/w3c/ServiceWorker/issues/1261 + // + if (IsBlobURI(mWorkerRef->Private()->GetBaseURI())) { + MOZ_DIAGNOSTIC_ASSERT(mLoader->GetController().isNothing()); + mLoader->SetController(mWorkerRef->Private()->GetParentController()); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +NetworkLoadHandler::OnStartRequest(nsIRequest* aRequest) { + nsresult rv = PrepareForRequest(aRequest); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRequest->Cancel(rv); + } + + return rv; +} + +nsresult NetworkLoadHandler::PrepareForRequest(nsIRequest* aRequest) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // If one load info cancels or hits an error, it can race with the start + // callback coming from another load info. + if (mRequestHandle->IsCancelled()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + + // Checking the MIME type is only required for ServiceWorkers' + // importScripts, per step 10 of + // https://w3c.github.io/ServiceWorker/#importscripts + // + // "Extract a MIME type from the response’s header list. If this MIME type + // (ignoring parameters) is not a JavaScript MIME type, return a network + // error." + if (mWorkerRef->Private()->IsServiceWorker()) { + nsAutoCString mimeType; + channel->GetContentType(mimeType); + + if (!nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(mimeType))) { + const nsCString& scope = mWorkerRef->Private() + ->GetServiceWorkerRegistrationDescriptor() + .Scope(); + + ServiceWorkerManager::LocalizeAndReportToAllClients( + scope, "ServiceWorkerRegisterMimeTypeError2", + nsTArray<nsString>{ + NS_ConvertUTF8toUTF16(scope), NS_ConvertUTF8toUTF16(mimeType), + NS_ConvertUTF8toUTF16(loadContext->mRequest->mURL)}); + + return NS_ERROR_DOM_NETWORK_ERR; + } + } + + // We synthesize the result code, but its never exposed to content. + SafeRefPtr<mozilla::dom::InternalResponse> ir = + MakeSafeRefPtr<mozilla::dom::InternalResponse>(200, "OK"_ns); + ir->SetBody(loadContext->mCacheReadStream, + InternalResponse::UNKNOWN_BODY_SIZE); + + // Drop our reference to the stream now that we've passed it along, so it + // doesn't hang around once the cache is done with it and keep data alive. + loadContext->mCacheReadStream = nullptr; + + // Set the channel info of the channel on the response so that it's + // saved in the cache. + ir->InitChannelInfo(channel); + + // Save the principal of the channel since its URI encodes the script URI + // rather than the ServiceWorkerRegistrationInfo URI. + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(ssm, "Should never be null!"); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + MOZ_TRY(ssm->GetChannelResultPrincipal(channel, + getter_AddRefs(channelPrincipal))); + + UniquePtr<PrincipalInfo> principalInfo(new PrincipalInfo()); + MOZ_TRY(PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get())); + + ir->SetPrincipalInfo(std::move(principalInfo)); + ir->Headers()->FillResponseHeaders(channel); + + RefPtr<mozilla::dom::Response> response = new mozilla::dom::Response( + mRequestHandle->GetCacheCreator()->Global(), std::move(ir), nullptr); + + mozilla::dom::RequestOrUSVString request; + + MOZ_ASSERT(!loadContext->mFullURL.IsEmpty()); + request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + + ErrorResult error; + RefPtr<Promise> cachePromise = + mRequestHandle->GetCacheCreator()->Cache_()->Put(jsapi.cx(), request, + *response, error); + error.WouldReportJSException(); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + RefPtr<CachePromiseHandler> promiseHandler = + new CachePromiseHandler(mLoader, mRequestHandle); + cachePromise->AppendNativeHandler(promiseHandler); + + loadContext->mCachePromise.swap(cachePromise); + loadContext->mCacheStatus = WorkerLoadContext::WritingToCache; + + return NS_OK; +} + +} // namespace workerinternals::loader + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/NetworkLoadHandler.h b/dom/workers/loader/NetworkLoadHandler.h new file mode 100644 index 0000000000..b32c9d8d8e --- /dev/null +++ b/dom/workers/loader/NetworkLoadHandler.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_NetworkLoadHandler_h__ +#define mozilla_dom_workers_NetworkLoadHandler_h__ + +#include "nsIStreamLoader.h" +#include "mozilla/dom/WorkerLoadContext.h" +#include "mozilla/dom/ScriptLoadHandler.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom::workerinternals::loader { + +class WorkerScriptLoader; + +/* + * [DOMDOC] NetworkLoadHandler for Workers + * + * A LoadHandler is a ScriptLoader helper class that reacts to an + * nsIStreamLoader's events for + * loading JS scripts. It is primarily responsible for decoding the stream into + * UTF8 or UTF16. Additionally, it takes care of any work that needs to follow + * the completion of a stream. Every LoadHandler also manages additional tasks + * for the type of load that it is doing. + * + * As part of worker loading we have an number of tasks that we need to take + * care of after a successfully completed stream, including setting a final URI + * on the WorkerPrivate if we have loaded a main script, or handling CSP issues. + * These are handled in DataReceivedFromNetwork, and implement roughly the same + * set of tasks as you will find in the CacheLoadhandler, which has a companion + * method DataReceivedFromcache. + * + * In the worker context, the LoadHandler is run on the main thread, and all + * work in this file ultimately is done by the main thread, including decoding. + * + */ + +class NetworkLoadHandler final : public nsIStreamLoaderObserver, + public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + + NetworkLoadHandler(WorkerScriptLoader* aLoader, + ThreadSafeRequestHandle* aRequestHandle); + + NS_IMETHOD + OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) override; + + nsresult DataReceivedFromNetwork(nsIStreamLoader* aLoader, nsresult aStatus, + uint32_t aStringLen, const uint8_t* aString); + + NS_IMETHOD + OnStartRequest(nsIRequest* aRequest) override; + + nsresult PrepareForRequest(nsIRequest* aRequest); + + NS_IMETHOD + OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) override { + // Nothing to do here! + return NS_OK; + } + + private: + ~NetworkLoadHandler() = default; + + RefPtr<WorkerScriptLoader> mLoader; + UniquePtr<ScriptDecoder> mDecoder; + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + RefPtr<ThreadSafeRequestHandle> mRequestHandle; +}; + +} // namespace mozilla::dom::workerinternals::loader + +#endif /* mozilla_dom_workers_NetworkLoadHandler_h__ */ diff --git a/dom/workers/loader/ScriptResponseHeaderProcessor.cpp b/dom/workers/loader/ScriptResponseHeaderProcessor.cpp new file mode 100644 index 0000000000..6dc5c9e1ef --- /dev/null +++ b/dom/workers/loader/ScriptResponseHeaderProcessor.cpp @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScriptResponseHeaderProcessor.h" +#include "mozilla/Try.h" +#include "mozilla/dom/WorkerScope.h" + +namespace mozilla { +namespace dom { + +namespace workerinternals { + +namespace loader { + +NS_IMPL_ISUPPORTS(ScriptResponseHeaderProcessor, nsIRequestObserver); + +nsresult ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( + WorkerPrivate* aWorkerPrivate, + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy, bool aIsMainScript) { + MOZ_ASSERT(aWorkerPrivate); + + if (aIsMainScript) { + MOZ_TRY(aWorkerPrivate->SetEmbedderPolicy(aPolicy)); + } else { + // NOTE: Spec doesn't mention non-main scripts must match COEP header with + // the main script, but it must pass CORP checking. + // see: wpt window-simple-success.https.html, the worker import script + // test-incrementer.js without coep header. + Unused << NS_WARN_IF(!aWorkerPrivate->MatchEmbedderPolicy(aPolicy)); + } + + return NS_OK; +} + +// Enforce strict MIME type checks for worker-imported scripts +// https://github.com/whatwg/html/pull/4001 +nsresult ScriptResponseHeaderProcessor::EnsureJavaScriptMimeType( + nsIRequest* aRequest) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + MOZ_ASSERT(channel); + nsAutoCString mimeType; + channel->GetContentType(mimeType); + if (!nsContentUtils::IsJavascriptMIMEType(NS_ConvertUTF8toUTF16(mimeType))) { + return NS_ERROR_DOM_NETWORK_ERR; + } + return NS_OK; +} + +nsresult ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( + nsIRequest* aRequest) { + nsCOMPtr<nsIHttpChannelInternal> httpChannel = do_QueryInterface(aRequest); + + // NOTE: the spec doesn't say what to do with non-HTTP workers. + // See: https://github.com/whatwg/html/issues/4916 + if (!httpChannel) { + if (mIsMainScript) { + mWorkerPrivate->InheritOwnerEmbedderPolicyOrNull(aRequest); + } + + return NS_OK; + } + + nsILoadInfo::CrossOriginEmbedderPolicy coep; + MOZ_TRY(httpChannel->GetResponseEmbedderPolicy( + mWorkerPrivate->Trials().IsEnabled(OriginTrial::CoepCredentialless), + &coep)); + + return ProcessCrossOriginEmbedderPolicyHeader(mWorkerPrivate, coep, + mIsMainScript); +} + +} // namespace loader +} // namespace workerinternals + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/ScriptResponseHeaderProcessor.h b/dom/workers/loader/ScriptResponseHeaderProcessor.h new file mode 100644 index 0000000000..43a9bfde42 --- /dev/null +++ b/dom/workers/loader/ScriptResponseHeaderProcessor.h @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_ScriptResponseHeaderProcessor_h__ +#define mozilla_dom_workers_ScriptResponseHeaderProcessor_h__ + +#include "mozilla/dom/WorkerCommon.h" + +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIStreamLoader.h" +#include "nsStreamUtils.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_dom.h" + +namespace mozilla::dom { + +class WorkerPrivate; + +namespace workerinternals::loader { + +/* ScriptResponseHeaderProcessor + * + * This class handles Policy headers. It can be used as a RequestObserver in a + * Tee, as it is for NetworkLoadHandler in WorkerScriptLoader, or the static + * method can be called directly, as it is in CacheLoadHandler. + * + */ + +class ScriptResponseHeaderProcessor final : public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + + ScriptResponseHeaderProcessor(WorkerPrivate* aWorkerPrivate, + bool aIsMainScript, bool aIsImportScript) + : mWorkerPrivate(aWorkerPrivate), + mIsMainScript(aIsMainScript), + mIsImportScript(aIsImportScript) { + AssertIsOnMainThread(); + } + + NS_IMETHOD OnStartRequest(nsIRequest* aRequest) override { + nsresult rv = NS_OK; + if (mIsImportScript && + StaticPrefs::dom_workers_importScripts_enforceStrictMimeType()) { + rv = EnsureJavaScriptMimeType(aRequest); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRequest->Cancel(rv); + return NS_OK; + } + } + + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return NS_OK; + } + + rv = ProcessCrossOriginEmbedderPolicyHeader(aRequest); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRequest->Cancel(rv); + } + + return rv; + } + + NS_IMETHOD OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) override { + return NS_OK; + } + + static nsresult ProcessCrossOriginEmbedderPolicyHeader( + WorkerPrivate* aWorkerPrivate, + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy, bool aIsMainScript); + + private: + ~ScriptResponseHeaderProcessor() = default; + + nsresult EnsureJavaScriptMimeType(nsIRequest* aRequest); + + nsresult ProcessCrossOriginEmbedderPolicyHeader(nsIRequest* aRequest); + + WorkerPrivate* const mWorkerPrivate; + const bool mIsMainScript; + const bool mIsImportScript; +}; + +} // namespace workerinternals::loader + +} // namespace mozilla::dom + +#endif /* mozilla_dom_workers_ScriptResponseHeaderProcessor_h__ */ diff --git a/dom/workers/loader/WorkerLoadContext.cpp b/dom/workers/loader/WorkerLoadContext.cpp new file mode 100644 index 0000000000..a788d5173c --- /dev/null +++ b/dom/workers/loader/WorkerLoadContext.cpp @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WorkerLoadContext.h" +#include "mozilla/dom/workerinternals/ScriptLoader.h" +#include "CacheLoadHandler.h" // CacheCreator + +namespace mozilla { +namespace dom { + +WorkerLoadContext::WorkerLoadContext( + Kind aKind, const Maybe<ClientInfo>& aClientInfo, + workerinternals::loader::WorkerScriptLoader* aScriptLoader, + bool aOnlyExistingCachedResourcesAllowed) + : JS::loader::LoadContextBase(JS::loader::ContextKind::Worker), + mKind(aKind), + mClientInfo(aClientInfo), + mScriptLoader(aScriptLoader), + mOnlyExistingCachedResourcesAllowed( + aOnlyExistingCachedResourcesAllowed){}; + +ThreadSafeRequestHandle::ThreadSafeRequestHandle( + JS::loader::ScriptLoadRequest* aRequest, nsISerialEventTarget* aSyncTarget) + : mRequest(aRequest), mOwningEventTarget(aSyncTarget) {} + +already_AddRefed<JS::loader::ScriptLoadRequest> +ThreadSafeRequestHandle::ReleaseRequest() { + RefPtr<JS::loader::ScriptLoadRequest> request; + mRequest.swap(request); + mRunnable = nullptr; + return request.forget(); +} + +nsresult ThreadSafeRequestHandle::OnStreamComplete(nsresult aStatus) { + return mRunnable->OnStreamComplete(this, aStatus); +} + +void ThreadSafeRequestHandle::LoadingFinished(nsresult aRv) { + mRunnable->LoadingFinished(this, aRv); +} + +void ThreadSafeRequestHandle::MaybeExecuteFinishedScripts() { + mRunnable->MaybeExecuteFinishedScripts(this); +} + +bool ThreadSafeRequestHandle::IsCancelled() { return mRunnable->IsCancelled(); } + +nsresult ThreadSafeRequestHandle::GetCancelResult() { + return mRunnable->GetCancelResult(); +} + +workerinternals::loader::CacheCreator* +ThreadSafeRequestHandle::GetCacheCreator() { + AssertIsOnMainThread(); + return mRunnable->GetCacheCreator(); +} + +ThreadSafeRequestHandle::~ThreadSafeRequestHandle() { + // Normally we only touch mStrongRef on the owning thread. This is safe, + // however, because when we do use mStrongRef on the owning thread we are + // always holding a strong ref to the ThreadsafeHandle via the owning + // runnable. So we cannot run the ThreadsafeHandle destructor simultaneously. + if (!mRequest || mOwningEventTarget->IsOnCurrentThread()) { + return; + } + + // Dispatch in NS_ProxyRelease is guaranteed to succeed here because we block + // shutdown until all Contexts have been destroyed. Therefore it is ok to have + // MOZ_ALWAYS_SUCCEED here. + MOZ_ALWAYS_SUCCEEDS(NS_ProxyRelease("ThreadSafeRequestHandle::mRequest", + mOwningEventTarget, mRequest.forget())); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/WorkerLoadContext.h b/dom/workers/loader/WorkerLoadContext.h new file mode 100644 index 0000000000..97362f2871 --- /dev/null +++ b/dom/workers/loader/WorkerLoadContext.h @@ -0,0 +1,219 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_WorkerLoadContext_h__ +#define mozilla_dom_workers_WorkerLoadContext_h__ + +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsIRequest.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/Promise.h" +#include "js/loader/ScriptKind.h" +#include "js/loader/ScriptLoadRequest.h" +#include "js/loader/LoadContextBase.h" + +class nsIReferrerInfo; +class nsIURI; + +namespace mozilla::dom { + +class ClientInfo; +class WorkerPrivate; + +namespace workerinternals::loader { +class CacheCreator; +class ScriptLoaderRunnable; +class WorkerScriptLoader; +} // namespace workerinternals::loader + +/* + * WorkerLoadContext (for all workers) + * + * LoadContexts augment the loading of a ScriptLoadRequest. They + * describe how a ScriptLoadRequests loading and evaluation needs to be + * augmented, based on the information provided by the loading context. The + * WorkerLoadContext has the following generic fields applied to all worker + * ScriptLoadRequests (and primarily used for error handling): + * + * * mMutedErrorFlag + * Set when we finish loading a script, and used to determine whether a + * given error is thrown or muted. + * * mLoadResult + * In order to report errors correctly in the worker thread, we need to + * move them from the main thread to the worker. This field records the + * load error, for throwing when we return to the worker thread. + * * mKind + * See documentation of WorkerLoadContext::Kind. + * * mClientInfo + * A snapshot of a global living in the system (see documentation for + * ClientInfo). In worker loading, this field is important for CSP + * information and knowing what to intercept for Service Worker + * interception. + * * mChannel + * The channel used by this request for it's load. Used for cancellation, + * in order to cancel the stream. + * + * The rest of the fields on this class focus on enabling the ServiceWorker + * usecase, in particular -- using the Cache API to store the worker so that + * in the case of (for example) a page refresh, the service worker itself is + * persisted so that it can do other work. For more details see the + * CacheLoadHandler.h file. + * + */ + +class WorkerLoadContext : public JS::loader::LoadContextBase { + public: + /* Worker Load Context Kinds + * + * A script that is loaded and run as a worker can be one of several species. + * Each may have slightly different behavior, but they fall into roughly two + * categories: the Main Worker Script (the script that triggers the first + * load) and scripts that are attached to this main worker script. + * + * In the specification, the Main Worker Script is referred to as the "top + * level script" and is defined here: + * https://html.spec.whatwg.org/multipage/webappapis.html#fetching-scripts-is-top-level + */ + + enum Kind { + // Indicates that the is-top-level bit is true. This may be a Classic script + // or a Module script. + MainScript, + // We are importing a script from the worker via ImportScript. This may only + // be a Classic script. + ImportScript, + // We are importing a script from the worker via a Static Import. This may + // only + // be a Module script. + StaticImport, + DynamicImport, + // We have an attached debugger, and these should be treated specially and + // not like a main script (regardless of their type). This is not part of + // the specification. + DebuggerScript + }; + + WorkerLoadContext(Kind aKind, const Maybe<ClientInfo>& aClientInfo, + workerinternals::loader::WorkerScriptLoader* aScriptLoader, + bool aOnlyExistingCachedResourcesAllowed); + + // Used to detect if the `is top-level` bit is set on a given module. + bool IsTopLevel() { + return mRequest->IsTopLevel() && (mKind == Kind::MainScript); + }; + + static Kind GetKind(bool isMainScript, bool isDebuggerScript) { + if (isDebuggerScript) { + return Kind::DebuggerScript; + } + if (isMainScript) { + return Kind::MainScript; + } + return Kind::ImportScript; + }; + + /* These fields are used by all workers */ + Maybe<bool> mMutedErrorFlag; + nsresult mLoadResult = NS_ERROR_NOT_INITIALIZED; + bool mLoadingFinished = false; + bool mIsTopLevel = true; + Kind mKind; + Maybe<ClientInfo> mClientInfo; + nsCOMPtr<nsIChannel> mChannel; + RefPtr<workerinternals::loader::WorkerScriptLoader> mScriptLoader; + + /* These fields are only used by service workers */ + /* TODO: Split out a ServiceWorkerLoadContext */ + // This full URL string is populated only if this object is used in a + // ServiceWorker. + nsString mFullURL; + + // This promise is set only when the script is for a ServiceWorker but + // it's not in the cache yet. The promise is resolved when the full body is + // stored into the cache. mCachePromise will be set to nullptr after + // resolution. + RefPtr<Promise> mCachePromise; + + // The reader stream the cache entry should be filled from, for those cases + // when we're going to have an mCachePromise. + nsCOMPtr<nsIInputStream> mCacheReadStream; + + enum CacheStatus { + // By default a normal script is just loaded from the network. But for + // ServiceWorkers, we have to check if the cache contains the script and + // load it from the cache. + Uncached, + + WritingToCache, + + ReadingFromCache, + + // This script has been loaded from the ServiceWorker cache. + Cached, + + // This script must be stored in the ServiceWorker cache. + ToBeCached, + + // Something went wrong or the worker went away. + Cancel + }; + + CacheStatus mCacheStatus = Uncached; + + // If the requested script is not currently in the cache, should we initiate + // a request to fetch and cache it? Only ServiceWorkers that are being + // installed are allowed to go to the network (and then cache the result). + bool mOnlyExistingCachedResourcesAllowed = false; + + bool IsAwaitingPromise() const { return bool(mCachePromise); } +}; + +class ThreadSafeRequestHandle final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ThreadSafeRequestHandle) + + ThreadSafeRequestHandle(JS::loader::ScriptLoadRequest* aRequest, + nsISerialEventTarget* aSyncTarget); + + JS::loader::ScriptLoadRequest* GetRequest() const { return mRequest; } + + WorkerLoadContext* GetContext() { return mRequest->GetWorkerLoadContext(); } + + bool IsEmpty() { return !mRequest; } + + // Runnable controls + nsresult OnStreamComplete(nsresult aStatus); + + void LoadingFinished(nsresult aRv); + + void MaybeExecuteFinishedScripts(); + + bool IsCancelled(); + + bool Finished() { + return GetContext()->mLoadingFinished && !GetContext()->IsAwaitingPromise(); + } + + nsresult GetCancelResult(); + + already_AddRefed<JS::loader::ScriptLoadRequest> ReleaseRequest(); + + workerinternals::loader::CacheCreator* GetCacheCreator(); + + RefPtr<workerinternals::loader::ScriptLoaderRunnable> mRunnable; + + bool mExecutionScheduled = false; + + private: + ~ThreadSafeRequestHandle(); + + RefPtr<JS::loader::ScriptLoadRequest> mRequest; + nsCOMPtr<nsISerialEventTarget> mOwningEventTarget; +}; + +} // namespace mozilla::dom +#endif /* mozilla_dom_workers_WorkerLoadContext_h__ */ diff --git a/dom/workers/loader/WorkerModuleLoader.cpp b/dom/workers/loader/WorkerModuleLoader.cpp new file mode 100644 index 0000000000..da340c89bd --- /dev/null +++ b/dom/workers/loader/WorkerModuleLoader.cpp @@ -0,0 +1,225 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "js/experimental/JSStencil.h" // JS::Stencil, JS::CompileModuleScriptToStencil, JS::InstantiateModuleStencil +#include "js/loader/ModuleLoadRequest.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/WorkerLoadContext.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/workerinternals/ScriptLoader.h" +#include "mozilla/dom/WorkerScope.h" +#include "WorkerModuleLoader.h" + +#include "nsISupportsImpl.h" + +namespace mozilla::dom::workerinternals::loader { + +////////////////////////////////////////////////////////////// +// WorkerModuleLoader +////////////////////////////////////////////////////////////// + +NS_IMPL_ADDREF_INHERITED(WorkerModuleLoader, JS::loader::ModuleLoaderBase) +NS_IMPL_RELEASE_INHERITED(WorkerModuleLoader, JS::loader::ModuleLoaderBase) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(WorkerModuleLoader, + JS::loader::ModuleLoaderBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkerModuleLoader) +NS_INTERFACE_MAP_END_INHERITING(JS::loader::ModuleLoaderBase) + +WorkerModuleLoader::WorkerModuleLoader(WorkerScriptLoader* aScriptLoader, + nsIGlobalObject* aGlobalObject) + : ModuleLoaderBase(aScriptLoader, aGlobalObject) {} + +nsIURI* WorkerModuleLoader::GetBaseURI() const { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + return workerPrivate->GetBaseURI(); +} + +already_AddRefed<ModuleLoadRequest> WorkerModuleLoader::CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) { + // We are intentionally deviating from the specification here and using the + // worker's CSP rather than the document CSP. The spec otherwise requires our + // service worker integration to be changed, and additionally the decision + // here did not make sense as we are treating static imports as different from + // other kinds of subresources. + // See Discussion in https://github.com/w3c/webappsec-csp/issues/336 + Maybe<ClientInfo> clientInfo = GetGlobalObject()->GetClientInfo(); + + RefPtr<WorkerLoadContext> loadContext = new WorkerLoadContext( + WorkerLoadContext::Kind::StaticImport, clientInfo, + aParent->GetWorkerLoadContext()->mScriptLoader, + aParent->GetWorkerLoadContext()->mOnlyExistingCachedResourcesAllowed); + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, aParent->ReferrerPolicy(), aParent->mFetchOptions, SRIMetadata(), + aParent->mURI, loadContext, false, /* is top level */ + false, /* is dynamic import */ + this, aParent->mVisitedSet, aParent->GetRootModule()); + + request->mURL = request->mURI->GetSpecOrDefault(); + request->NoCacheEntryFound(); + return request.forget(); +} + +bool WorkerModuleLoader::CreateDynamicImportLoader() { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + workerPrivate->AssertIsOnWorkerThread(); + + IgnoredErrorResult rv; + RefPtr<WorkerScriptLoader> loader = loader::WorkerScriptLoader::Create( + workerPrivate, nullptr, nullptr, + GetCurrentScriptLoader()->GetWorkerScriptType(), rv); + if (NS_WARN_IF(rv.Failed())) { + return false; + } + + SetScriptLoader(loader); + return true; +} + +already_AddRefed<ModuleLoadRequest> WorkerModuleLoader::CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + + if (!CreateDynamicImportLoader()) { + return nullptr; + } + + // Not supported for Service Workers. + // https://github.com/w3c/ServiceWorker/issues/1585 covers existing discussion + // about potentially supporting use of import(). + if (workerPrivate->IsServiceWorker()) { + return nullptr; + } + MOZ_ASSERT(aSpecifier); + MOZ_ASSERT(aPromise); + + RefPtr<ScriptFetchOptions> options; + nsIURI* baseURL = nullptr; + if (aMaybeActiveScript) { + // https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule + // Step 6.3. Set fetchOptions to the new descendant script fetch options for + // referencingScript's fetch options. + options = aMaybeActiveScript->GetFetchOptions(); + baseURL = aMaybeActiveScript->BaseURL(); + } else { + // https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule + // Step 4. Let fetchOptions be the default classic script fetch options. + // + // https://html.spec.whatwg.org/multipage/webappapis.html#default-classic-script-fetch-options + // The default classic script fetch options are a script fetch options whose + // cryptographic nonce is the empty string, integrity metadata is the empty + // string, parser metadata is "not-parser-inserted", credentials mode is + // "same-origin", referrer policy is the empty string, and fetch priority is + // "auto". + options = new ScriptFetchOptions( + CORSMode::CORS_NONE, /* aNonce = */ u""_ns, RequestPriority::Auto, + JS::loader::ParserMetadata::NotParserInserted, nullptr); + baseURL = GetBaseURI(); + } + + Maybe<ClientInfo> clientInfo = GetGlobalObject()->GetClientInfo(); + + RefPtr<WorkerLoadContext> context = new WorkerLoadContext( + WorkerLoadContext::Kind::DynamicImport, clientInfo, + GetCurrentScriptLoader(), + // When dynamic import is supported in ServiceWorkers, + // the current plan in onlyExistingCachedResourcesAllowed + // is that only existing cached resources will be + // allowed. (`import()` will not be used for caching + // side effects, but instead a specific method will be + // used during installation.) + true); + + ReferrerPolicy referrerPolicy = workerPrivate->GetReferrerPolicy(); + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, referrerPolicy, options, SRIMetadata(), baseURL, context, true, + /* is top level */ true, /* is dynamic import */ + this, ModuleLoadRequest::NewVisitedSetForTopLevelImport(aURI), nullptr); + + request->SetDynamicImport(aMaybeActiveScript, aSpecifier, aPromise); + request->NoCacheEntryFound(); + + return request.forget(); +} + +bool WorkerModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, + nsresult* aRvOut) { + return true; +} + +nsresult WorkerModuleLoader::StartFetch(ModuleLoadRequest* aRequest) { + if (!GetScriptLoaderFor(aRequest)->DispatchLoadScript(aRequest)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult WorkerModuleLoader::CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions, + ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleScript) { + RefPtr<JS::Stencil> stencil; + MOZ_ASSERT(aRequest->IsTextSource()); + MaybeSourceText maybeSource; + nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource, + aRequest->mLoadContext.get()); + NS_ENSURE_SUCCESS(rv, rv); + + auto compile = [&](auto& source) { + return JS::CompileModuleScriptToStencil(aCx, aOptions, source); + }; + stencil = maybeSource.mapNonEmpty(compile); + + if (!stencil) { + return NS_ERROR_FAILURE; + } + + JS::InstantiateOptions instantiateOptions(aOptions); + aModuleScript.set( + JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil)); + if (!aModuleScript) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +WorkerScriptLoader* WorkerModuleLoader::GetCurrentScriptLoader() { + return static_cast<WorkerScriptLoader*>(mLoader.get()); +} + +WorkerScriptLoader* WorkerModuleLoader::GetScriptLoaderFor( + ModuleLoadRequest* aRequest) { + return aRequest->GetWorkerLoadContext()->mScriptLoader; +} + +void WorkerModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) { + if (aRequest->IsTopLevel()) { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(GetGlobalObject()))) { + return; + } + RefPtr<WorkerScriptLoader> requestScriptLoader = + GetScriptLoaderFor(aRequest); + if (aRequest->IsDynamicImport()) { + aRequest->ProcessDynamicImport(); + requestScriptLoader->TryShutdown(); + } else { + requestScriptLoader->MaybeMoveToLoadedList(aRequest); + requestScriptLoader->ProcessPendingRequests(jsapi.cx()); + } + } +} + +bool WorkerModuleLoader::IsModuleEvaluationAborted( + ModuleLoadRequest* aRequest) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + return !workerPrivate || !workerPrivate->GlobalScope() || + workerPrivate->GlobalScope()->IsDying(); +} + +} // namespace mozilla::dom::workerinternals::loader diff --git a/dom/workers/loader/WorkerModuleLoader.h b/dom/workers/loader/WorkerModuleLoader.h new file mode 100644 index 0000000000..6ad45b42a0 --- /dev/null +++ b/dom/workers/loader/WorkerModuleLoader.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_loader_WorkerModuleLoader_h +#define mozilla_loader_WorkerModuleLoader_h + +#include "js/loader/ModuleLoaderBase.h" +#include "js/loader/ScriptFetchOptions.h" +#include "mozilla/dom/SerializedStackHolder.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla::dom::workerinternals::loader { +class WorkerScriptLoader; + +// alias common classes +using ScriptFetchOptions = JS::loader::ScriptFetchOptions; +using ScriptKind = JS::loader::ScriptKind; +using ScriptLoadRequest = JS::loader::ScriptLoadRequest; +using ScriptLoadRequestList = JS::loader::ScriptLoadRequestList; +using ModuleLoadRequest = JS::loader::ModuleLoadRequest; + +// WorkerModuleLoader +// +// The WorkerModuleLoader provides the methods that implement specification +// step 5 from "To fetch a worklet/module worker script graph", specifically for +// workers. In addition, this implements worker specific initialization for +// Static imports and Dynamic imports. +// +// The steps are outlined in "To fetch the descendants of and link a module +// script" and are common for all Modules. Thus we delegate to ModuleLoaderBase +// for those steps. +class WorkerModuleLoader : public JS::loader::ModuleLoaderBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WorkerModuleLoader, + JS::loader::ModuleLoaderBase) + + WorkerModuleLoader(WorkerScriptLoader* aScriptLoader, + nsIGlobalObject* aGlobalObject); + + private: + ~WorkerModuleLoader() = default; + + bool CreateDynamicImportLoader(); + void SetScriptLoader(JS::loader::ScriptLoaderInterface* aLoader) { + mLoader = aLoader; + } + + WorkerScriptLoader* GetCurrentScriptLoader(); + + WorkerScriptLoader* GetScriptLoaderFor(ModuleLoadRequest* aRequest); + + nsIURI* GetBaseURI() const override; + + already_AddRefed<ModuleLoadRequest> CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) override; + + already_AddRefed<ModuleLoadRequest> CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JSString*> aSpecifier, + JS::Handle<JSObject*> aPromise) override; + + bool CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) override; + + // StartFetch is special for worker modules, as we need to move back to the + // main thread to start a new load. + nsresult StartFetch(ModuleLoadRequest* aRequest) override; + + nsresult CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, + JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, + JS::MutableHandle<JSObject*> aModuleScript) override; + + void OnModuleLoadComplete(ModuleLoadRequest* aRequest) override; + + bool IsModuleEvaluationAborted(ModuleLoadRequest* aRequest) override; +}; + +} // namespace mozilla::dom::workerinternals::loader +#endif // mozilla_loader_WorkerModuleLoader_h diff --git a/dom/workers/loader/moz.build b/dom/workers/loader/moz.build new file mode 100644 index 0000000000..d39a6acb24 --- /dev/null +++ b/dom/workers/loader/moz.build @@ -0,0 +1,34 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Workers") + +# Public stuff. +EXPORTS.mozilla.dom += [ + "WorkerLoadContext.h", +] + +# Private stuff. +EXPORTS.mozilla.dom.workerinternals += [ + "CacheLoadHandler.h", + "NetworkLoadHandler.h", + "ScriptResponseHeaderProcessor.h", + "WorkerModuleLoader.h", +] + +UNIFIED_SOURCES += [ + "CacheLoadHandler.cpp", + "NetworkLoadHandler.cpp", + "ScriptResponseHeaderProcessor.cpp", + "WorkerLoadContext.cpp", + "WorkerModuleLoader.cpp", +] + + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/workers/moz.build b/dom/workers/moz.build new file mode 100644 index 0000000000..c7818826d1 --- /dev/null +++ b/dom/workers/moz.build @@ -0,0 +1,114 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Workers") + +DIRS += ["remoteworkers", "sharedworkers", "loader"] + +# Public stuff. +EXPORTS.mozilla.dom += [ + "ChromeWorker.h", + "EventWithOptionsRunnable.h", + "JSExecutionManager.h", + "Worker.h", + "WorkerChannelInfo.h", + "WorkerCommon.h", + "WorkerDebugger.h", + "WorkerDebuggerManager.h", + "WorkerDocumentListener.h", + "WorkerError.h", + "WorkerIPCUtils.h", + "WorkerLoadInfo.h", + "WorkerLocation.h", + "WorkerNavigator.h", + "WorkerPrivate.h", + "WorkerRef.h", + "WorkerRunnable.h", + "WorkerScope.h", + "WorkerStatus.h", + "WorkerTestUtils.h", +] + +# Private stuff. +EXPORTS.mozilla.dom.workerinternals += [ + "JSSettings.h", + "Queue.h", + "RuntimeService.h", + "ScriptLoader.h", +] + +XPIDL_MODULE = "dom_workers" + +XPIDL_SOURCES += [ + "nsIWorkerChannelInfo.idl", + "nsIWorkerDebugger.idl", + "nsIWorkerDebuggerManager.idl", +] + +UNIFIED_SOURCES += [ + "ChromeWorker.cpp", + "ChromeWorkerScope.cpp", + "EventWithOptionsRunnable.cpp", + "JSExecutionManager.cpp", + "MessageEventRunnable.cpp", + "RegisterBindings.cpp", + "RuntimeService.cpp", + "ScriptLoader.cpp", + "Worker.cpp", + "WorkerChannelInfo.cpp", + "WorkerCSPEventListener.cpp", + "WorkerDebugger.cpp", + "WorkerDebuggerManager.cpp", + "WorkerDocumentListener.cpp", + "WorkerError.cpp", + "WorkerEventTarget.cpp", + "WorkerLoadInfo.cpp", + "WorkerLocation.cpp", + "WorkerNavigator.cpp", + "WorkerPrivate.cpp", + "WorkerRef.cpp", + "WorkerRunnable.cpp", + "WorkerScope.cpp", + "WorkerTestUtils.cpp", + "WorkerThread.cpp", +] + +LOCAL_INCLUDES += [ + "/caps", + "/dom/base", + "/dom/bindings", + "/dom/system", + "/dom/workers/remoteworkers", + "/js/xpconnect/loader", + "/netwerk/base", + "/xpcom/build", + "/xpcom/threads", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + LOCAL_INCLUDES += [ + "/xpcom/base", + ] + + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +MOCHITEST_MANIFESTS += [ + "test/mochitest.toml", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "test/chrome.toml", +] + +MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] diff --git a/dom/workers/nsIWorkerChannelInfo.idl b/dom/workers/nsIWorkerChannelInfo.idl new file mode 100644 index 0000000000..5cf3305749 --- /dev/null +++ b/dom/workers/nsIWorkerChannelInfo.idl @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl BrowsingContext; + +[scriptable, builtinclass, uuid(bf9a175a-03bc-4d7b-ba2f-76347cf40d7b)] +interface nsIWorkerChannelLoadInfo : nsISupports +{ + [infallible] attribute unsigned long long workerAssociatedBrowsingContextID; + [infallible] readonly attribute BrowsingContext workerAssociatedBrowsingContext; +}; + +[scriptable, builtinclass, uuid(df1fffe4-dac6-487e-979a-629ac8c64831)] +interface nsIWorkerChannelInfo : nsISupports +{ + attribute nsIWorkerChannelLoadInfo loadInfo; + [must_use] readonly attribute uint64_t channelId; +}; diff --git a/dom/workers/nsIWorkerDebugger.idl b/dom/workers/nsIWorkerDebugger.idl new file mode 100644 index 0000000000..931d01a4ac --- /dev/null +++ b/dom/workers/nsIWorkerDebugger.idl @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindow; +interface nsIPrincipal; + +[scriptable, uuid(9cf3b48e-361d-486a-8917-55cf8d00bb41)] +interface nsIWorkerDebuggerListener : nsISupports +{ + void onClose(); + + void onError(in AString filename, in unsigned long lineno, + in AString message); + + void onMessage(in AString message); +}; + +[scriptable, builtinclass, uuid(22f93aa3-8a05-46be-87e0-fa93bf8a8eff)] +interface nsIWorkerDebugger : nsISupports +{ + const unsigned long TYPE_DEDICATED = 0; + const unsigned long TYPE_SHARED = 1; + const unsigned long TYPE_SERVICE = 2; + + readonly attribute bool isClosed; + + readonly attribute bool isChrome; + + readonly attribute bool isInitialized; + + readonly attribute nsIWorkerDebugger parent; + + readonly attribute unsigned long type; + + readonly attribute AString url; + + // If this is a dedicated worker, the window this worker or (in the case of + // nested workers) its top-level ancestral worker is associated with. + readonly attribute mozIDOMWindow window; + + readonly attribute Array<uint64_t> windowIDs; + + readonly attribute nsIPrincipal principal; + + readonly attribute unsigned long serviceWorkerID; + + readonly attribute AString id; + + void initialize(in AString url); + + [binaryname(PostMessageMoz)] + void postMessage(in AString message); + + void addListener(in nsIWorkerDebuggerListener listener); + + void removeListener(in nsIWorkerDebuggerListener listener); + + // Indicate whether the debugger has finished initializing. By default the + // debugger will be considered initialized when the onRegister hooks in all + // nsIWorkerDebuggerManagerListener have been called. + // + // setDebuggerReady(false) can be called during an onRegister hook to mark + // the debugger as not being ready yet. This will prevent all content from + // running in the worker, including the worker's main script and any messages + // posted to it. Other runnables will still execute in the worker as normal. + // + // When the debugger is ready, setDebuggerReady(true) should then be called + // to allow the worker to begin executing content. + void setDebuggerReady(in boolean ready); +}; diff --git a/dom/workers/nsIWorkerDebuggerManager.idl b/dom/workers/nsIWorkerDebuggerManager.idl new file mode 100644 index 0000000000..ce047cfdc9 --- /dev/null +++ b/dom/workers/nsIWorkerDebuggerManager.idl @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsISimpleEnumerator; +interface nsIWorkerDebugger; + +[scriptable, uuid(d2aa74ee-6b98-4d5d-8173-4e23422daf1e)] +interface nsIWorkerDebuggerManagerListener : nsISupports +{ + void onRegister(in nsIWorkerDebugger aDebugger); + + void onUnregister(in nsIWorkerDebugger aDebugger); +}; + +[scriptable, builtinclass, uuid(056d7918-dc86-452a-b4e6-86da3405f015)] +interface nsIWorkerDebuggerManager : nsISupports +{ + nsISimpleEnumerator getWorkerDebuggerEnumerator(); + + void addListener(in nsIWorkerDebuggerManagerListener listener); + + void removeListener(in nsIWorkerDebuggerManagerListener listener); +}; diff --git a/dom/workers/remoteworkers/PRemoteWorker.ipdl b/dom/workers/remoteworkers/PRemoteWorker.ipdl new file mode 100644 index 0000000000..4da11c4840 --- /dev/null +++ b/dom/workers/remoteworkers/PRemoteWorker.ipdl @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; +include protocol PFetchEventOpProxy; + +include DOMTypes; +include ServiceWorkerOpArgs; +include RemoteWorkerTypes; + +namespace mozilla { +namespace dom { + +struct RemoteWorkerSuspendOp +{}; + +struct RemoteWorkerResumeOp +{}; + +struct RemoteWorkerFreezeOp +{}; + +struct RemoteWorkerThawOp +{}; + +struct RemoteWorkerTerminateOp +{}; + +struct RemoteWorkerPortIdentifierOp +{ + MessagePortIdentifier portIdentifier; +}; + +struct RemoteWorkerAddWindowIDOp +{ + uint64_t windowID; +}; + +struct RemoteWorkerRemoveWindowIDOp +{ + uint64_t windowID; +}; + +union RemoteWorkerOp { + RemoteWorkerSuspendOp; + RemoteWorkerResumeOp; + RemoteWorkerFreezeOp; + RemoteWorkerThawOp; + RemoteWorkerTerminateOp; + RemoteWorkerPortIdentifierOp; + RemoteWorkerAddWindowIDOp; + RemoteWorkerRemoveWindowIDOp; +}; + +// This protocol is used to make a remote worker controllable from the parent +// process. The parent process will receive operations from the +// PRemoteWorkerController protocol. +protocol PRemoteWorker +{ + manager PBackground; + + manages PFetchEventOpProxy; + +parent: + async Created(bool aStatus); + + async Error(ErrorValue aValue); + + async NotifyLock(bool aCreated); + + async NotifyWebTransport(bool aCreated); + + async Close(); + + async SetServiceWorkerSkipWaitingFlag() returns (bool aOk); + +child: + async PFetchEventOpProxy(ParentToChildServiceWorkerFetchEventOpArgs aArgs); + + async __delete__(); + + async ExecOp(RemoteWorkerOp op); + + async ExecServiceWorkerOp(ServiceWorkerOpArgs aArgs) + returns (ServiceWorkerOpResult aResult); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/PRemoteWorkerController.ipdl b/dom/workers/remoteworkers/PRemoteWorkerController.ipdl new file mode 100644 index 0000000000..059a81f8fe --- /dev/null +++ b/dom/workers/remoteworkers/PRemoteWorkerController.ipdl @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; +include protocol PFetchEventOp; + +include RemoteWorkerTypes; +include ServiceWorkerOpArgs; + +namespace mozilla { +namespace dom { + +/** + * Proxy protocol used by ServiceWorkerManager whose canonical state exists on + * the main thread to control/receive feedback from RemoteWorkers which are + * canonically controlled from the PBackground thread. Exclusively for use from + * the parent process main thread to the parent process PBackground thread. + */ +protocol PRemoteWorkerController { + manager PBackground; + + manages PFetchEventOp; + + child: + async CreationFailed(); + + async CreationSucceeded(); + + async ErrorReceived(ErrorValue aError); + + async Terminated(); + + async SetServiceWorkerSkipWaitingFlag() returns (bool aOk); + + parent: + async PFetchEventOp(ParentToParentServiceWorkerFetchEventOpArgs aArgs); + + async __delete__(); + + async Shutdown() returns (bool aOk); + + async ExecServiceWorkerOp(ServiceWorkerOpArgs aArgs) + returns (ServiceWorkerOpResult aResult); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/PRemoteWorkerService.ipdl b/dom/workers/remoteworkers/PRemoteWorkerService.ipdl new file mode 100644 index 0000000000..6287bb56a1 --- /dev/null +++ b/dom/workers/remoteworkers/PRemoteWorkerService.ipdl @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include ProtocolTypes; +include RemoteWorkerTypes; + +namespace mozilla { +namespace dom { + +// Simple protocol to register any active RemoteWorkerService running on any +// process. Initialization/registration is delayed for preallocated processes +// until the process takes on its final remoteType. +protocol PRemoteWorkerService +{ + manager PBackground; + +parent: + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerChild.cpp b/dom/workers/remoteworkers/RemoteWorkerChild.cpp new file mode 100644 index 0000000000..a644439a8d --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerChild.cpp @@ -0,0 +1,1071 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerChild.h" + +#include <utility> + +#include "MainThreadUtils.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIConsoleReportCollector.h" +#include "nsIInterfaceRequestor.h" +#include "nsIPrincipal.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +#include "RemoteWorkerService.h" +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/FetchEventOpProxyChild.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/RemoteWorkerManager.h" // RemoteWorkerManager::IsRemoteTypeAllowed +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/dom/ServiceWorkerInterceptController.h" +#include "mozilla/dom/ServiceWorkerOp.h" +#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h" +#include "mozilla/dom/ServiceWorkerShutdownState.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/workerinternals/ScriptLoader.h" +#include "mozilla/dom/WorkerError.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/PermissionManager.h" + +mozilla::LazyLogModule gRemoteWorkerChildLog("RemoteWorkerChild"); + +#ifdef LOG +# undef LOG +#endif +#define LOG(fmt) MOZ_LOG(gRemoteWorkerChildLog, mozilla::LogLevel::Verbose, fmt) + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +using workerinternals::ChannelFromScriptURLMainThread; + +namespace { + +class SharedWorkerInterfaceRequestor final : public nsIInterfaceRequestor { + public: + NS_DECL_ISUPPORTS + + SharedWorkerInterfaceRequestor() { + // This check must match the code nsDocShell::Create. + if (XRE_IsParentProcess()) { + mSWController = new ServiceWorkerInterceptController(); + } + } + + NS_IMETHOD + GetInterface(const nsIID& aIID, void** aSink) override { + MOZ_ASSERT(NS_IsMainThread()); + + if (mSWController && + aIID.Equals(NS_GET_IID(nsINetworkInterceptController))) { + // If asked for the network intercept controller, ask the outer requestor, + // which could be the docshell. + RefPtr<ServiceWorkerInterceptController> swController = mSWController; + swController.forget(aSink); + return NS_OK; + } + + return NS_NOINTERFACE; + } + + private: + ~SharedWorkerInterfaceRequestor() = default; + + RefPtr<ServiceWorkerInterceptController> mSWController; +}; + +NS_IMPL_ADDREF(SharedWorkerInterfaceRequestor) +NS_IMPL_RELEASE(SharedWorkerInterfaceRequestor) +NS_IMPL_QUERY_INTERFACE(SharedWorkerInterfaceRequestor, nsIInterfaceRequestor) + +// Normal runnable because AddPortIdentifier() is going to exec JS code. +class MessagePortIdentifierRunnable final : public WorkerRunnable { + public: + MessagePortIdentifierRunnable(WorkerPrivate* aWorkerPrivate, + RemoteWorkerChild* aActor, + const MessagePortIdentifier& aPortIdentifier) + : WorkerRunnable(aWorkerPrivate, "MessagePortIdentifierRunnable"), + mActor(aActor), + mPortIdentifier(aPortIdentifier) {} + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + if (aWorkerPrivate->GlobalScope()->IsDying()) { + mPortIdentifier.ForceClose(); + return true; + } + mActor->AddPortIdentifier(aCx, aWorkerPrivate, mPortIdentifier); + return true; + } + + RefPtr<RemoteWorkerChild> mActor; + UniqueMessagePortId mPortIdentifier; +}; + +// This is used to propagate the CSP violation when loading the SharedWorker +// main-script and nothing else. +class RemoteWorkerCSPEventListener final : public nsICSPEventListener { + public: + NS_DECL_ISUPPORTS + + explicit RemoteWorkerCSPEventListener(RemoteWorkerChild* aActor) + : mActor(aActor){}; + + NS_IMETHOD OnCSPViolationEvent(const nsAString& aJSON) override { + mActor->CSPViolationPropagationOnMainThread(aJSON); + return NS_OK; + } + + private: + ~RemoteWorkerCSPEventListener() = default; + + RefPtr<RemoteWorkerChild> mActor; +}; + +NS_IMPL_ISUPPORTS(RemoteWorkerCSPEventListener, nsICSPEventListener) + +} // anonymous namespace + +RemoteWorkerChild::RemoteWorkerChild(const RemoteWorkerData& aData) + : mState(VariantType<Pending>(), "RemoteWorkerChild::mState"), + mServiceKeepAlive(RemoteWorkerService::MaybeGetKeepAlive()), + mIsServiceWorker(aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData) { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); +} + +RemoteWorkerChild::~RemoteWorkerChild() { +#ifdef DEBUG + auto lock = mState.Lock(); + MOZ_ASSERT(lock->is<Killed>()); +#endif +} + +void RemoteWorkerChild::ActorDestroy(ActorDestroyReason) { + auto launcherData = mLauncherData.Access(); + + Unused << NS_WARN_IF(!launcherData->mTerminationPromise.IsEmpty()); + launcherData->mTerminationPromise.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, + __func__); + + auto lock = mState.Lock(); + + // If the worker hasn't shutdown or begun shutdown, we need to ensure it gets + // canceled. + if (NS_WARN_IF(!lock->is<Killed>() && !lock->is<Canceled>())) { + // In terms of strong references to this RemoteWorkerChild, at this moment: + // - IPC is holding a strong reference that will be dropped in the near + // future after this method returns. + // - If the worker has been started by ExecWorkerOnMainThread, then + // WorkerPrivate::mRemoteWorkerController is a strong reference to us. + // If the worker has not been started, ExecWorker's runnable lambda will + // have a strong reference that will cover the call to + // ExecWorkerOnMainThread. + // - The WorkerPrivate cancellation and termination callbacks will also + // hold strong references, but those callbacks will not outlive the + // WorkerPrivate and are not exposed to callers like + // mRemoteWorkerController is. + // + // Note that this call to RequestWorkerCancellation can still race worker + // cancellation, in which case the strong reference obtained by + // NewRunnableMethod can end up being the last strong reference. + // (RequestWorkerCancellation handles the case that the Worker is already + // canceled if this happens.) + RefPtr<nsIRunnable> runnable = + NewRunnableMethod("RequestWorkerCancellation", this, + &RemoteWorkerChild::RequestWorkerCancellation); + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(runnable.forget())); + } +} + +void RemoteWorkerChild::ExecWorker(const RemoteWorkerData& aData) { +#ifdef DEBUG + MOZ_ASSERT(GetActorEventTarget()->IsOnCurrentThread()); + auto launcherData = mLauncherData.Access(); + MOZ_ASSERT(CanSend()); +#endif + + RefPtr<RemoteWorkerChild> self = this; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), data = aData]() mutable { + nsresult rv = self->ExecWorkerOnMainThread(std::move(data)); + + // Creation failure will already have been reported via the method + // above internally using ScopeExit. + Unused << NS_WARN_IF(NS_FAILED(rv)); + }); + + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); +} + +nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) { + MOZ_ASSERT(NS_IsMainThread()); + + // Ensure that the IndexedDatabaseManager is initialized so that if any + // workers do any IndexedDB calls that all of IDB's prefs/etc. are + // initialized. + Unused << NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate()); + + auto scopeExit = + MakeScopeExit([&] { ExceptionalErrorTransitionDuringExecWorker(); }); + + // Verify the the RemoteWorker should be really allowed to run in this + // process, and fail if it shouldn't (This shouldn't normally happen, + // unless the RemoteWorkerData has been tempered in the process it was + // sent from). + if (!RemoteWorkerManager::IsRemoteTypeAllowed(aData)) { + return NS_ERROR_UNEXPECTED; + } + + auto principalOrErr = PrincipalInfoToPrincipal(aData.principalInfo()); + if (NS_WARN_IF(principalOrErr.isErr())) { + return principalOrErr.unwrapErr(); + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + auto loadingPrincipalOrErr = + PrincipalInfoToPrincipal(aData.loadingPrincipalInfo()); + if (NS_WARN_IF(loadingPrincipalOrErr.isErr())) { + return loadingPrincipalOrErr.unwrapErr(); + } + + auto partitionedPrincipalOrErr = + PrincipalInfoToPrincipal(aData.partitionedPrincipalInfo()); + if (NS_WARN_IF(partitionedPrincipalOrErr.isErr())) { + return partitionedPrincipalOrErr.unwrapErr(); + } + + WorkerLoadInfo info; + info.mBaseURI = DeserializeURI(aData.baseScriptURL()); + info.mResolvedScriptURI = DeserializeURI(aData.resolvedScriptURL()); + + info.mPrincipalInfo = MakeUnique<PrincipalInfo>(aData.principalInfo()); + info.mPartitionedPrincipalInfo = + MakeUnique<PrincipalInfo>(aData.partitionedPrincipalInfo()); + + info.mReferrerInfo = aData.referrerInfo(); + info.mDomain = aData.domain(); + info.mTrials = aData.originTrials(); + info.mPrincipal = principal; + info.mPartitionedPrincipal = partitionedPrincipalOrErr.unwrap(); + info.mLoadingPrincipal = loadingPrincipalOrErr.unwrap(); + info.mStorageAccess = aData.storageAccess(); + info.mUseRegularPrincipal = aData.useRegularPrincipal(); + info.mUsingStorageAccess = aData.usingStorageAccess(); + info.mIsThirdPartyContextToTopWindow = aData.isThirdPartyContextToTopWindow(); + info.mOriginAttributes = + BasePrincipal::Cast(principal)->OriginAttributesRef(); + info.mShouldResistFingerprinting = aData.shouldResistFingerprinting(); + Maybe<RFPTarget> overriddenFingerprintingSettings; + if (aData.overriddenFingerprintingSettings().isSome()) { + overriddenFingerprintingSettings.emplace( + RFPTarget(aData.overriddenFingerprintingSettings().ref())); + } + info.mOverriddenFingerprintingSettings = overriddenFingerprintingSettings; + net::CookieJarSettings::Deserialize(aData.cookieJarSettings(), + getter_AddRefs(info.mCookieJarSettings)); + info.mCookieJarSettingsArgs = aData.cookieJarSettings(); + + // Default CSP permissions for now. These will be overrided if necessary + // based on the script CSP headers during load in ScriptLoader. + info.mEvalAllowed = true; + info.mReportEvalCSPViolations = false; + info.mWasmEvalAllowed = true; + info.mReportWasmEvalCSPViolations = false; + info.mSecureContext = aData.isSecureContext() + ? WorkerLoadInfo::eSecureContext + : WorkerLoadInfo::eInsecureContext; + + WorkerPrivate::OverrideLoadInfoLoadGroup(info, info.mLoadingPrincipal); + + RefPtr<SharedWorkerInterfaceRequestor> requestor = + new SharedWorkerInterfaceRequestor(); + info.mInterfaceRequestor->SetOuterRequestor(requestor); + + Maybe<ClientInfo> clientInfo; + if (aData.clientInfo().isSome()) { + clientInfo.emplace(ClientInfo(aData.clientInfo().ref())); + } + + nsresult rv = NS_OK; + + if (clientInfo.isSome()) { + Maybe<mozilla::ipc::CSPInfo> cspInfo = clientInfo.ref().GetCspInfo(); + if (cspInfo.isSome()) { + info.mCSP = CSPInfoToCSP(cspInfo.ref(), nullptr); + info.mCSPInfo = MakeUnique<CSPInfo>(); + rv = CSPToCSPInfo(info.mCSP, info.mCSPInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + rv = info.SetPrincipalsAndCSPOnMainThread( + info.mPrincipal, info.mPartitionedPrincipal, info.mLoadGroup, info.mCSP); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString workerPrivateId; + + if (mIsServiceWorker) { + ServiceWorkerData& data = aData.serviceWorkerData().get_ServiceWorkerData(); + + MOZ_ASSERT(!data.id().IsEmpty()); + workerPrivateId = std::move(data.id()); + + info.mServiceWorkerCacheName = data.cacheName(); + info.mServiceWorkerDescriptor.emplace(data.descriptor()); + info.mServiceWorkerRegistrationDescriptor.emplace( + data.registrationDescriptor()); + info.mLoadFlags = static_cast<nsLoadFlags>(data.loadFlags()); + } else { + // Top level workers' main script use the document charset for the script + // uri encoding. + rv = ChannelFromScriptURLMainThread( + info.mLoadingPrincipal, nullptr /* parent document */, info.mLoadGroup, + info.mResolvedScriptURI, aData.type(), aData.credentials(), clientInfo, + nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER, info.mCookieJarSettings, + info.mReferrerInfo, getter_AddRefs(info.mChannel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsILoadInfo> loadInfo = info.mChannel->LoadInfo(); + + auto* cspEventListener = new RemoteWorkerCSPEventListener(this); + rv = loadInfo->SetCspEventListener(cspEventListener); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + info.mAgentClusterId = aData.agentClusterId(); + + AutoJSAPI jsapi; + jsapi.Init(); + + ErrorResult error; + RefPtr<RemoteWorkerChild> self = this; + RefPtr<WorkerPrivate> workerPrivate = WorkerPrivate::Constructor( + jsapi.cx(), aData.originalScriptURL(), false, + mIsServiceWorker ? WorkerKindService : WorkerKindShared, + aData.credentials(), aData.type(), aData.name(), VoidCString(), &info, + error, std::move(workerPrivateId), + [self](bool aEverRan) { + self->OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled(); + }, + // This will be invoked here on the main thread when the worker is already + // fully shutdown. This replaces a prior approach where we would + // begin to transition when the worker thread would reach the Canceling + // state. This lambda ensures that we not only wait for the Killing state + // to be reached but that the global shutdown has already occurred. + [self]() { self->TransitionStateFromCanceledToKilled(); }); + + if (NS_WARN_IF(error.Failed())) { + MOZ_ASSERT(!workerPrivate); + + rv = error.StealNSResult(); + return rv; + } + + workerPrivate->SetRemoteWorkerController(this); + + // This wants to run as a normal task sequentially after the top level script + // evaluation, so the hybrid target is the correct choice between hybrid and + // `ControlEventTarget`. + nsCOMPtr<nsISerialEventTarget> workerTarget = + workerPrivate->HybridEventTarget(); + + nsCOMPtr<nsIRunnable> runnable = NewCancelableRunnableMethod( + "InitialzeOnWorker", this, &RemoteWorkerChild::InitializeOnWorker); + + { + MOZ_ASSERT(workerPrivate); + auto lock = mState.Lock(); + // We MUST be pending here, so direct access is ok. + lock->as<Pending>().mWorkerPrivate = std::move(workerPrivate); + } + + if (mIsServiceWorker) { + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [workerTarget, + initializeWorkerRunnable = std::move(runnable)]() mutable { + Unused << NS_WARN_IF(NS_FAILED( + workerTarget->Dispatch(initializeWorkerRunnable.forget()))); + }); + + RefPtr<PermissionManager> permissionManager = + PermissionManager::GetInstance(); + if (!permissionManager) { + return NS_ERROR_FAILURE; + } + permissionManager->WhenPermissionsAvailable(principal, r); + } else { + if (NS_WARN_IF(NS_FAILED(workerTarget->Dispatch(runnable.forget())))) { + rv = NS_ERROR_FAILURE; + return rv; + } + } + + scopeExit.release(); + + return NS_OK; +} + +void RemoteWorkerChild::RequestWorkerCancellation() { + MOZ_ASSERT(NS_IsMainThread()); + + LOG(("RequestWorkerCancellation[this=%p]", this)); + + // We want to ensure that we've requested the worker be canceled. So if the + // worker is running, cancel it. We can't do this with the lock held, + // however, because our lambdas will want to manipulate the state. + RefPtr<WorkerPrivate> cancelWith; + { + auto lock = mState.Lock(); + if (lock->is<Pending>()) { + cancelWith = lock->as<Pending>().mWorkerPrivate; + } else if (lock->is<Running>()) { + cancelWith = lock->as<Running>().mWorkerPrivate; + } + } + + if (cancelWith) { + cancelWith->Cancel(); + } +} + +// This method will be invoked on the worker after the top-level +// CompileScriptRunnable task has succeeded and as long as the worker has not +// been closed/canceled. There are edge-cases related to cancellation, but we +// have our caller ensure that we are only called as long as the worker's state +// is Running. +// +// (https://bugzilla.mozilla.org/show_bug.cgi?id=1800659 will eliminate +// cancellation, and the documentation around that bug / in design documents +// helps provide more context about this.) +void RemoteWorkerChild::InitializeOnWorker() { + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("TransitionStateToRunning", this, + &RemoteWorkerChild::TransitionStateToRunning); + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); +} + +RefPtr<GenericNonExclusivePromise> RemoteWorkerChild::GetTerminationPromise() { + auto launcherData = mLauncherData.Access(); + return launcherData->mTerminationPromise.Ensure(__func__); +} + +void RemoteWorkerChild::CreationSucceededOnAnyThread() { + CreationSucceededOrFailedOnAnyThread(true); +} + +void RemoteWorkerChild::CreationFailedOnAnyThread() { + CreationSucceededOrFailedOnAnyThread(false); +} + +void RemoteWorkerChild::CreationSucceededOrFailedOnAnyThread( + bool aDidCreationSucceed) { +#ifdef DEBUG + { + auto lock = mState.Lock(); + MOZ_ASSERT_IF(aDidCreationSucceed, lock->is<Running>()); + MOZ_ASSERT_IF(!aDidCreationSucceed, lock->is<Killed>()); + } +#endif + + RefPtr<RemoteWorkerChild> self = this; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, + [self = std::move(self), didCreationSucceed = aDidCreationSucceed] { + auto launcherData = self->mLauncherData.Access(); + + if (!self->CanSend() || launcherData->mDidSendCreated) { + return; + } + + Unused << self->SendCreated(didCreationSucceed); + launcherData->mDidSendCreated = true; + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::CloseWorkerOnMainThread() { + AssertIsOnMainThread(); + + LOG(("CloseWorkerOnMainThread[this=%p]", this)); + + // We can't hold the state lock while calling WorkerPrivate::Cancel because + // the lambda callback will want to touch the state, so save off the + // WorkerPrivate so we can cancel it (if we need to cancel it). + RefPtr<WorkerPrivate> cancelWith; + { + auto lock = mState.Lock(); + + if (lock->is<Pending>()) { + cancelWith = lock->as<Pending>().mWorkerPrivate; + // There should be no way for this code to run before we + // ExecWorkerOnMainThread runs, which means that either it should have + // set a WorkerPrivate on Pending, or its error handling should already + // have transitioned us to Canceled and Killing in that order. (It's + // also possible that it assigned a WorkerPrivate and subsequently we + // transitioned to Running, which would put us in the next branch.) + MOZ_DIAGNOSTIC_ASSERT(cancelWith); + } else if (lock->is<Running>()) { + cancelWith = lock->as<Running>().mWorkerPrivate; + } + } + + // It's very okay for us to not have a WorkerPrivate here if we've already + // canceled the worker or if errors happened. + if (cancelWith) { + cancelWith->Cancel(); + } +} + +/** + * Error reporting method + */ +void RemoteWorkerChild::ErrorPropagation(const ErrorValue& aValue) { + MOZ_ASSERT(GetActorEventTarget()->IsOnCurrentThread()); + + if (!CanSend()) { + return; + } + + Unused << SendError(aValue); +} + +void RemoteWorkerChild::ErrorPropagationDispatch(nsresult aError) { + MOZ_ASSERT(NS_FAILED(aError)); + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "RemoteWorkerChild::ErrorPropagationDispatch", + [self = std::move(self), aError]() { self->ErrorPropagation(aError); }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::ErrorPropagationOnMainThread( + const WorkerErrorReport* aReport, bool aIsErrorEvent) { + AssertIsOnMainThread(); + + ErrorValue value; + if (aIsErrorEvent) { + ErrorData data( + aReport->mIsWarning, aReport->mLineNumber, aReport->mColumnNumber, + aReport->mMessage, aReport->mFilename, aReport->mLine, + TransformIntoNewArray(aReport->mNotes, [](const WorkerErrorNote& note) { + return ErrorDataNote(note.mLineNumber, note.mColumnNumber, + note.mMessage, note.mFilename); + })); + value = data; + } else { + value = void_t(); + } + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "RemoteWorkerChild::ErrorPropagationOnMainThread", + [self = std::move(self), value]() { self->ErrorPropagation(value); }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::CSPViolationPropagationOnMainThread( + const nsAString& aJSON) { + AssertIsOnMainThread(); + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "RemoteWorkerChild::ErrorPropagationDispatch", + [self = std::move(self), json = nsString(aJSON)]() { + CSPViolation violation(json); + self->ErrorPropagation(violation); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::NotifyLock(bool aCreated) { + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [self = RefPtr(this), aCreated] { + if (!self->CanSend()) { + return; + } + + Unused << self->SendNotifyLock(aCreated); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::NotifyWebTransport(bool aCreated) { + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [self = RefPtr(this), aCreated] { + if (!self->CanSend()) { + return; + } + + Unused << self->SendNotifyWebTransport(aCreated); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::FlushReportsOnMainThread( + nsIConsoleReportCollector* aReporter) { + AssertIsOnMainThread(); + + bool reportErrorToBrowserConsole = true; + + // Flush the reports. + for (uint32_t i = 0, len = mWindowIDs.Length(); i < len; ++i) { + aReporter->FlushReportsToConsole( + mWindowIDs[i], nsIConsoleReportCollector::ReportAction::Save); + reportErrorToBrowserConsole = false; + } + + // Finally report to browser console if there is no any window. + if (reportErrorToBrowserConsole) { + aReporter->FlushReportsToConsole(0); + return; + } + + aReporter->ClearConsoleReports(); +} + +/** + * Worker state transition methods + */ +RemoteWorkerChild::WorkerPrivateAccessibleState:: + ~WorkerPrivateAccessibleState() { + // We should now only be performing state transitions on the main thread, so + // we should assert we're only releasing on the main thread. + MOZ_ASSERT(!mWorkerPrivate || NS_IsMainThread()); + // mWorkerPrivate can be safely released on the main thread. + if (!mWorkerPrivate || NS_IsMainThread()) { + return; + } + + // But as a backstop, do proxy the release to the main thread. + NS_ReleaseOnMainThread( + "RemoteWorkerChild::WorkerPrivateAccessibleState::mWorkerPrivate", + mWorkerPrivate.forget()); +} + +void RemoteWorkerChild:: + OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled() { + auto lock = mState.Lock(); + + LOG(("TransitionStateFromPendingOrRunningToCanceled[this=%p]", this)); + + if (lock->is<Pending>()) { + TransitionStateFromPendingToCanceled(lock.ref()); + } else if (lock->is<Running>()) { + *lock = VariantType<Canceled>(); + } else { + MOZ_ASSERT(false, "State should have been Pending or Running"); + } +} + +void RemoteWorkerChild::TransitionStateFromPendingToCanceled(State& aState) { + AssertIsOnMainThread(); + MOZ_ASSERT(aState.is<Pending>()); + LOG(("TransitionStateFromPendingToCanceled[this=%p]", this)); + + CancelAllPendingOps(aState); + + aState = VariantType<Canceled>(); +} + +void RemoteWorkerChild::TransitionStateFromCanceledToKilled() { + AssertIsOnMainThread(); + + LOG(("TransitionStateFromCanceledToKilled[this=%p]", this)); + + auto lock = mState.Lock(); + MOZ_ASSERT(lock->is<Canceled>()); + + *lock = VariantType<Killed>(); + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self]() { + auto launcherData = self->mLauncherData.Access(); + + // (We maintain the historical ordering of resolving this promise prior to + // calling SendClose, however the previous code used 2 separate dispatches + // to this thread for the resolve and SendClose, and there inherently + // would be a race between the runnables resulting from the resolved + // promise and the promise containing the call to SendClose. Now it's + // entirely clear that our call to SendClose will effectively run before + // any of the resolved promises are able to do anything.) + launcherData->mTerminationPromise.ResolveIfExists(true, __func__); + + if (self->CanSend()) { + Unused << self->SendClose(); + } + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::TransitionStateToRunning() { + AssertIsOnMainThread(); + + LOG(("TransitionStateToRunning[this=%p]", this)); + + nsTArray<RefPtr<Op>> pendingOps; + + { + auto lock = mState.Lock(); + + // Because this is an async notification sent from the worker to the main + // thread, it's very possible that we've already decided on the main thread + // to transition to the Canceled state, in which case there is nothing for + // us to do here. + if (!lock->is<Pending>()) { + LOG(("State is already not pending in TransitionStateToRunning[this=%p]!", + this)); + return; + } + + RefPtr<WorkerPrivate> workerPrivate = + std::move(lock->as<Pending>().mWorkerPrivate); + pendingOps = std::move(lock->as<Pending>().mPendingOps); + + // Move the worker private into place to avoid gratuitous ref churn; prior + // comments here suggest the Variant can't accept a move. + *lock = VariantType<Running>(); + lock->as<Running>().mWorkerPrivate = std::move(workerPrivate); + } + + CreationSucceededOnAnyThread(); + + RefPtr<RemoteWorkerChild> self = this; + for (auto& op : pendingOps) { + op->StartOnMainThread(self); + } +} + +void RemoteWorkerChild::ExceptionalErrorTransitionDuringExecWorker() { + AssertIsOnMainThread(); + + LOG(("ExceptionalErrorTransitionDuringExecWorker[this=%p]", this)); + + // This method is called synchronously by ExecWorkerOnMainThread in the event + // of any error. Because we only transition to Running on the main thread + // as the result of a notification from the worker, we know our state will be + // Pending, but mWorkerPrivate may or may not be null, as we may not have + // gotten to spawning the worker. + // + // In the event the worker exists, we need to Cancel() it. We must do this + // without the lock held because our call to Cancel() will invoke the + // cancellation callback we created which will call TransitionStateToCanceled, + // and we can't be holding the lock when that happens. + + RefPtr<WorkerPrivate> cancelWith; + + { + auto lock = mState.Lock(); + + MOZ_ASSERT(lock->is<Pending>()); + if (lock->is<Pending>()) { + cancelWith = lock->as<Pending>().mWorkerPrivate; + if (!cancelWith) { + // The worker wasn't actually created, so we should synthetically + // transition to canceled and onward. Since we have the lock, + // perform the transition now for clarity, but we'll handle the rest of + // this case after dropping the lock. + TransitionStateFromPendingToCanceled(lock.ref()); + } + } + } + + if (cancelWith) { + cancelWith->Cancel(); + } else { + TransitionStateFromCanceledToKilled(); + CreationFailedOnAnyThread(); + } +} + +/** + * Operation execution classes/methods + */ +class RemoteWorkerChild::SharedWorkerOp : public RemoteWorkerChild::Op { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedWorkerOp, override) + + explicit SharedWorkerOp(RemoteWorkerOp&& aOp) : mOp(std::move(aOp)) {} + + bool MaybeStart(RemoteWorkerChild* aOwner, + RemoteWorkerChild::State& aState) override { + MOZ_ASSERT(!mStarted); + MOZ_ASSERT(aOwner); + // Thread: We are on the Worker Launcher thread. + + // Return false, indicating we should queue this op if our current state is + // pending and this isn't a termination op (which should skip the line). + if (aState.is<Pending>() && !IsTerminationOp()) { + return false; + } + + // If the worker is already shutting down (which should be unexpected + // because we should be told new operations after a termination op), just + // return true to indicate the op should be discarded. + if (aState.is<Canceled>() || aState.is<Killed>()) { +#ifdef DEBUG + mStarted = true; +#endif + if (mOp.type() == RemoteWorkerOp::TRemoteWorkerPortIdentifierOp) { + MessagePort::ForceClose( + mOp.get_RemoteWorkerPortIdentifierOp().portIdentifier()); + } + return true; + } + + MOZ_ASSERT(aState.is<Running>() || IsTerminationOp()); + + RefPtr<SharedWorkerOp> self = this; + RefPtr<RemoteWorkerChild> owner = aOwner; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), owner = std::move(owner)]() mutable { + { + auto lock = owner->mState.Lock(); + + if (NS_WARN_IF(lock->is<Canceled>() || lock->is<Killed>())) { + self->Cancel(); + // Worker has already canceled, force close the MessagePort. + if (self->mOp.type() == + RemoteWorkerOp::TRemoteWorkerPortIdentifierOp) { + MessagePort::ForceClose( + self->mOp.get_RemoteWorkerPortIdentifierOp() + .portIdentifier()); + } + return; + } + } + + self->StartOnMainThread(owner); + }); + + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); + +#ifdef DEBUG + mStarted = true; +#endif + + return true; + } + + void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) final { + using Running = RemoteWorkerChild::Running; + + AssertIsOnMainThread(); + + if (IsTerminationOp()) { + aOwner->CloseWorkerOnMainThread(); + return; + } + + auto lock = aOwner->mState.Lock(); + MOZ_ASSERT(lock->is<Running>()); + if (!lock->is<Running>()) { + aOwner->ErrorPropagationDispatch(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<WorkerPrivate> workerPrivate = lock->as<Running>().mWorkerPrivate; + + MOZ_ASSERT(workerPrivate); + + if (mOp.type() == RemoteWorkerOp::TRemoteWorkerSuspendOp) { + workerPrivate->ParentWindowPaused(); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerResumeOp) { + workerPrivate->ParentWindowResumed(); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerFreezeOp) { + workerPrivate->Freeze(nullptr); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerThawOp) { + workerPrivate->Thaw(nullptr); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerPortIdentifierOp) { + RefPtr<MessagePortIdentifierRunnable> r = + new MessagePortIdentifierRunnable( + workerPrivate, aOwner, + mOp.get_RemoteWorkerPortIdentifierOp().portIdentifier()); + if (NS_WARN_IF(!r->Dispatch())) { + aOwner->ErrorPropagationDispatch(NS_ERROR_FAILURE); + } + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerAddWindowIDOp) { + aOwner->mWindowIDs.AppendElement( + mOp.get_RemoteWorkerAddWindowIDOp().windowID()); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerRemoveWindowIDOp) { + aOwner->mWindowIDs.RemoveElement( + mOp.get_RemoteWorkerRemoveWindowIDOp().windowID()); + } else { + MOZ_CRASH("Unknown RemoteWorkerOp type!"); + } + } + + void Cancel() override { +#ifdef DEBUG + mStarted = true; +#endif + } + + private: + ~SharedWorkerOp() { MOZ_ASSERT(mStarted); } + + bool IsTerminationOp() const { + return mOp.type() == RemoteWorkerOp::TRemoteWorkerTerminateOp; + } + + RemoteWorkerOp mOp; + +#ifdef DEBUG + bool mStarted = false; +#endif +}; + +void RemoteWorkerChild::AddPortIdentifier( + JSContext* aCx, WorkerPrivate* aWorkerPrivate, + UniqueMessagePortId& aPortIdentifier) { + if (NS_WARN_IF(!aWorkerPrivate->ConnectMessagePort(aCx, aPortIdentifier))) { + ErrorPropagationDispatch(NS_ERROR_FAILURE); + } +} + +void RemoteWorkerChild::CancelAllPendingOps(State& aState) { + MOZ_ASSERT(aState.is<Pending>()); + + auto pendingOps = std::move(aState.as<Pending>().mPendingOps); + + for (auto& op : pendingOps) { + op->Cancel(); + } +} + +void RemoteWorkerChild::MaybeStartOp(RefPtr<Op>&& aOp) { + MOZ_ASSERT(aOp); + + auto lock = mState.Lock(); + + if (!aOp->MaybeStart(this, lock.ref())) { + // Maybestart returns false only if we are <Pending>. + lock->as<Pending>().mPendingOps.AppendElement(std::move(aOp)); + } +} + +IPCResult RemoteWorkerChild::RecvExecOp(RemoteWorkerOp&& aOp) { + MOZ_ASSERT(!mIsServiceWorker); + + MaybeStartOp(new SharedWorkerOp(std::move(aOp))); + + return IPC_OK(); +} + +IPCResult RemoteWorkerChild::RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve) { + MOZ_ASSERT(mIsServiceWorker); + MOZ_ASSERT( + aArgs.type() != + ServiceWorkerOpArgs::TParentToChildServiceWorkerFetchEventOpArgs, + "FetchEvent operations should be sent via PFetchEventOp(Proxy) actors!"); + + MaybeReportServiceWorkerShutdownProgress(aArgs); + + MaybeStartOp(ServiceWorkerOp::Create(std::move(aArgs), std::move(aResolve))); + + return IPC_OK(); +} + +RefPtr<GenericPromise> +RemoteWorkerChild::MaybeSendSetServiceWorkerSkipWaitingFlag() { + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + RefPtr<RemoteWorkerChild> self = this; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self = std::move( + self), + promise] { + if (!self->CanSend()) { + promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + return; + } + + self->SendSetServiceWorkerSkipWaitingFlag()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]( + const SetServiceWorkerSkipWaitingFlagPromise::ResolveOrRejectValue& + aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + return; + } + + promise->Resolve(aResult.ResolveValue(), __func__); + }); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + + return promise; +} + +/** + * PFetchEventOpProxy methods + */ +already_AddRefed<PFetchEventOpProxyChild> +RemoteWorkerChild::AllocPFetchEventOpProxyChild( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) { + return RefPtr{new FetchEventOpProxyChild()}.forget(); +} + +IPCResult RemoteWorkerChild::RecvPFetchEventOpProxyConstructor( + PFetchEventOpProxyChild* aActor, + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) { + MOZ_ASSERT(aActor); + + (static_cast<FetchEventOpProxyChild*>(aActor))->Initialize(aArgs); + + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerChild.h b/dom/workers/remoteworkers/RemoteWorkerChild.h new file mode 100644 index 0000000000..cdb2fb32d7 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerChild.h @@ -0,0 +1,239 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerChild_h +#define mozilla_dom_RemoteWorkerChild_h + +#include "nsCOMPtr.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +#include "mozilla/DataMutex.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ThreadBound.h" +#include "mozilla/dom/PRemoteWorkerChild.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" + +class nsISerialEventTarget; +class nsIConsoleReportCollector; + +namespace mozilla::dom { + +class ErrorValue; +class FetchEventOpProxyChild; +class RemoteWorkerData; +class RemoteWorkerServiceKeepAlive; +class ServiceWorkerOp; +class UniqueMessagePortId; +class WeakWorkerRef; +class WorkerErrorReport; +class WorkerPrivate; + +/** + * Background-managed "Worker Launcher"-thread-resident created via the + * RemoteWorkerManager to actually spawn the worker. Currently, the worker will + * be spawned from the main thread due to nsIPrincipal not being able to be + * created on background threads and other ownership invariants, most of which + * can be relaxed in the future. + */ +class RemoteWorkerChild final : public PRemoteWorkerChild { + friend class FetchEventOpProxyChild; + friend class PRemoteWorkerChild; + friend class ServiceWorkerOp; + + ~RemoteWorkerChild(); + + public: + // Note that all IPC-using methods must only be invoked on the + // RemoteWorkerService thread which the inherited + // IProtocol::GetActorEventTarget() will return for us. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteWorkerChild, final) + + explicit RemoteWorkerChild(const RemoteWorkerData& aData); + + void ExecWorker(const RemoteWorkerData& aData); + + void ErrorPropagationOnMainThread(const WorkerErrorReport* aReport, + bool aIsErrorEvent); + + void CSPViolationPropagationOnMainThread(const nsAString& aJSON); + + void NotifyLock(bool aCreated); + + void NotifyWebTransport(bool aCreated); + + void FlushReportsOnMainThread(nsIConsoleReportCollector* aReporter); + + void AddPortIdentifier(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + UniqueMessagePortId& aPortIdentifier); + + RefPtr<GenericNonExclusivePromise> GetTerminationPromise(); + + RefPtr<GenericPromise> MaybeSendSetServiceWorkerSkipWaitingFlag(); + + const nsTArray<uint64_t>& WindowIDs() const { return mWindowIDs; } + + private: + class InitializeWorkerRunnable; + + class Op; + class SharedWorkerOp; + + struct WorkerPrivateAccessibleState { + ~WorkerPrivateAccessibleState(); + RefPtr<WorkerPrivate> mWorkerPrivate; + }; + + // Initial state, mWorkerPrivate is initially null but will be initialized on + // the main thread by ExecWorkerOnMainThread when the WorkerPrivate is + // created. The state will transition to Running or Canceled, also from the + // main thread. + struct Pending : WorkerPrivateAccessibleState { + nsTArray<RefPtr<Op>> mPendingOps; + }; + + // Running, with the state transition happening on the main thread as a result + // of the worker successfully processing our initialization runnable, + // indicating that top-level script execution successfully completed. Because + // all of our state transitions happen on the main thread and are posed in + // terms of the main thread's perspective of the worker's state, it's very + // possible for us to skip directly from Pending to Canceled because we decide + // to cancel/terminate the worker prior to it finishing script loading or + // reporting back to us. + struct Running : WorkerPrivateAccessibleState {}; + + // Cancel() has been called on the WorkerPrivate on the main thread by a + // TerminationOp, top-level script evaluation has failed and canceled the + // worker, or in the case of a SharedWorker, close() has been called on + // the global scope by content code and the worker has advanced to the + // Canceling state. (Dedicated Workers can also self close, but they will + // never be RemoteWorkers. Although a SharedWorker can own DedicatedWorkers.) + // Browser shutdown will result in a TerminationOp thanks to use of a shutdown + // blocker in the parent, so the RuntimeService shouldn't get involved, but we + // would also handle that case acceptably too. + // + // Because worker self-closing is still handled by dispatching a runnable to + // the main thread to effectively call WorkerPrivate::Cancel(), there isn't + // a race between a worker deciding to self-close and our termination ops. + // + // In this state, we have dropped the reference to the WorkerPrivate and will + // no longer be dispatching runnables to the worker. We wait in this state + // until the termination lambda is invoked letting us know that the worker has + // entirely shutdown and we can advanced to the Killed state. + struct Canceled {}; + + // The worker termination lambda has been invoked and we know the Worker is + // entirely shutdown. (Inherently it is possible for us to advance to this + // state while the nsThread for the worker is still in the process of + // shutting down, but no more worker code will run on it.) + // + // This name is chosen to match the Worker's own state model. + struct Killed {}; + + using State = Variant<Pending, Running, Canceled, Killed>; + + // The state of the WorkerPrivate as perceived by the owner on the main + // thread. All state transitions now happen on the main thread, but the + // Worker Launcher thread will consult the state and will directly append ops + // to the Pending queue + DataMutex<State> mState; + + const RefPtr<RemoteWorkerServiceKeepAlive> mServiceKeepAlive; + + class Op { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual ~Op() = default; + + virtual bool MaybeStart(RemoteWorkerChild* aOwner, State& aState) = 0; + + virtual void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) = 0; + + virtual void Cancel() = 0; + }; + + void ActorDestroy(ActorDestroyReason) override; + + mozilla::ipc::IPCResult RecvExecOp(RemoteWorkerOp&& aOp); + + mozilla::ipc::IPCResult RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve); + + already_AddRefed<PFetchEventOpProxyChild> AllocPFetchEventOpProxyChild( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs); + + mozilla::ipc::IPCResult RecvPFetchEventOpProxyConstructor( + PFetchEventOpProxyChild* aActor, + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) override; + + nsresult ExecWorkerOnMainThread(RemoteWorkerData&& aData); + + void ExceptionalErrorTransitionDuringExecWorker(); + + void RequestWorkerCancellation(); + + void InitializeOnWorker(); + + void CreationSucceededOnAnyThread(); + + void CreationFailedOnAnyThread(); + + void CreationSucceededOrFailedOnAnyThread(bool aDidCreationSucceed); + + // Cancels the worker if it has been started and ensures that we transition + // to the Terminated state once the worker has been terminated or we have + // ensured that it will never start. + void CloseWorkerOnMainThread(); + + void ErrorPropagation(const ErrorValue& aValue); + + void ErrorPropagationDispatch(nsresult aError); + + // When the WorkerPrivate Cancellation lambda is invoked, it's possible that + // we have not yet advanced to running from pending, so we could be in either + // state. This method is expected to be called by the Workers' cancellation + // lambda and will obtain the lock and call the + // TransitionStateFromPendingToCanceled if appropriate. Otherwise it will + // directly move from the running state to the canceled state which does not + // require additional cleanup. + void OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled(); + // A helper used by the above method by the worker cancellation lambda if the + // the worker hasn't started running, or in exceptional cases where we bail + // out of the ExecWorker method early. The caller must be holding the lock + // (in order to pass in the state). + void TransitionStateFromPendingToCanceled(State& aState); + void TransitionStateFromCanceledToKilled(); + + void TransitionStateToRunning(); + + void TransitionStateToTerminated(); + + void TransitionStateToTerminated(State& aState); + + void CancelAllPendingOps(State& aState); + + void MaybeStartOp(RefPtr<Op>&& aOp); + + const bool mIsServiceWorker; + + // Touched on main-thread only. + nsTArray<uint64_t> mWindowIDs; + + struct LauncherBoundData { + MozPromiseHolder<GenericNonExclusivePromise> mTerminationPromise; + // Flag to ensure we report creation at most once. This could be cleaned up + // further. + bool mDidSendCreated = false; + }; + + ThreadBound<LauncherBoundData> mLauncherData; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerChild_h diff --git a/dom/workers/remoteworkers/RemoteWorkerController.cpp b/dom/workers/remoteworkers/RemoteWorkerController.cpp new file mode 100644 index 0000000000..b0d56aa33d --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerController.cpp @@ -0,0 +1,573 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerController.h" + +#include <utility> + +#include "nsDebug.h" + +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Maybe.h" +#include "mozilla/RemoteLazyInputStreamStorage.h" +#include "mozilla/dom/FetchEventOpParent.h" +#include "mozilla/dom/FetchEventOpProxyParent.h" +#include "mozilla/dom/MessagePortParent.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/ServiceWorkerCloneData.h" +#include "mozilla/dom/ServiceWorkerShutdownState.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "RemoteWorkerControllerParent.h" +#include "RemoteWorkerManager.h" +#include "RemoteWorkerParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +/* static */ +already_AddRefed<RemoteWorkerController> RemoteWorkerController::Create( + const RemoteWorkerData& aData, RemoteWorkerObserver* aObserver, + base::ProcessId aProcessId) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aObserver); + + RefPtr<RemoteWorkerController> controller = + new RemoteWorkerController(aData, aObserver); + + RefPtr<RemoteWorkerManager> manager = RemoteWorkerManager::GetOrCreate(); + MOZ_ASSERT(manager); + + // XXX: We do not check for failure here, should we? + manager->Launch(controller, aData, aProcessId); + + return controller.forget(); +} + +RemoteWorkerController::RemoteWorkerController(const RemoteWorkerData& aData, + RemoteWorkerObserver* aObserver) + : mObserver(aObserver), + mState(ePending), + mIsServiceWorker(aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); +} + +RemoteWorkerController::~RemoteWorkerController() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mPendingOps.IsEmpty()); +} + +void RemoteWorkerController::SetWorkerActor(RemoteWorkerParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActor); + MOZ_ASSERT(aActor); + + mActor = aActor; +} + +void RemoteWorkerController::NoteDeadWorkerActor() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActor); + + // The actor has been destroyed without a proper close() notification. Let's + // inform the observer. + if (mState == eReady) { + mObserver->Terminated(); + } + + mActor = nullptr; + + Shutdown(); +} + +void RemoteWorkerController::CreationFailed() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(mState == ePending || mState == eTerminated); + + if (mState == eTerminated) { + MOZ_ASSERT(!mActor); + MOZ_ASSERT(mPendingOps.IsEmpty()); + // Nothing to do. + return; + } + + NoteDeadWorker(); + + mObserver->CreationFailed(); +} + +void RemoteWorkerController::CreationSucceeded() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == ePending || mState == eTerminated); + + if (mState == eTerminated) { + MOZ_ASSERT(!mActor); + MOZ_ASSERT(mPendingOps.IsEmpty()); + // Nothing to do. + return; + } + + MOZ_ASSERT(mActor); + mState = eReady; + + mObserver->CreationSucceeded(); + + auto pendingOps = std::move(mPendingOps); + + for (auto& op : pendingOps) { + DebugOnly<bool> started = op->MaybeStart(this); + MOZ_ASSERT(started); + } +} + +void RemoteWorkerController::ErrorPropagation(const ErrorValue& aValue) { + AssertIsOnBackgroundThread(); + + mObserver->ErrorReceived(aValue); +} + +void RemoteWorkerController::NotifyLock(bool aCreated) { + AssertIsOnBackgroundThread(); + + mObserver->LockNotified(aCreated); +} + +void RemoteWorkerController::NotifyWebTransport(bool aCreated) { + AssertIsOnBackgroundThread(); + + mObserver->WebTransportNotified(aCreated); +} + +void RemoteWorkerController::WorkerTerminated() { + AssertIsOnBackgroundThread(); + + NoteDeadWorker(); + + mObserver->Terminated(); +} + +void RemoteWorkerController::CancelAllPendingOps() { + AssertIsOnBackgroundThread(); + + auto pendingOps = std::move(mPendingOps); + + for (auto& op : pendingOps) { + op->Cancel(); + } +} + +void RemoteWorkerController::Shutdown() { + AssertIsOnBackgroundThread(); + Unused << NS_WARN_IF(mIsServiceWorker && !mPendingOps.IsEmpty()); + + if (mState == eTerminated) { + MOZ_ASSERT(mPendingOps.IsEmpty()); + return; + } + + mState = eTerminated; + + CancelAllPendingOps(); + + if (!mActor) { + return; + } + + mActor->SetController(nullptr); + + /** + * The "non-remote-side" of the Service Worker will have ensured that the + * remote worker is terminated before calling `Shutdown().` + */ + if (mIsServiceWorker) { + mActor->MaybeSendDelete(); + } else { + Unused << mActor->SendExecOp(RemoteWorkerTerminateOp()); + } + + mActor = nullptr; +} + +void RemoteWorkerController::NoteDeadWorker() { + AssertIsOnBackgroundThread(); + + CancelAllPendingOps(); + + /** + * The "non-remote-side" of the Service Worker will initiate `Shutdown()` + * once it's notified that all dispatched operations have either completed + * or canceled. That is, it'll explicitly call `Shutdown()` later. + */ + if (!mIsServiceWorker) { + Shutdown(); + } +} + +template <typename... Args> +void RemoteWorkerController::MaybeStartSharedWorkerOp(Args&&... aArgs) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mIsServiceWorker); + + UniquePtr<PendingSharedWorkerOp> op = + MakeUnique<PendingSharedWorkerOp>(std::forward<Args>(aArgs)...); + + if (!op->MaybeStart(this)) { + mPendingOps.AppendElement(std::move(op)); + } +} + +void RemoteWorkerController::AddWindowID(uint64_t aWindowID) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aWindowID); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eAddWindowID, aWindowID); +} + +void RemoteWorkerController::RemoveWindowID(uint64_t aWindowID) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aWindowID); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eRemoveWindowID, aWindowID); +} + +void RemoteWorkerController::AddPortIdentifier( + const MessagePortIdentifier& aPortIdentifier) { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(aPortIdentifier); +} + +void RemoteWorkerController::Terminate() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eTerminate); +} + +void RemoteWorkerController::Suspend() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eSuspend); +} + +void RemoteWorkerController::Resume() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eResume); +} + +void RemoteWorkerController::Freeze() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eFreeze); +} + +void RemoteWorkerController::Thaw() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eThaw); +} + +RefPtr<ServiceWorkerOpPromise> RemoteWorkerController::ExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIsServiceWorker); + + RefPtr<ServiceWorkerOpPromise::Private> promise = + new ServiceWorkerOpPromise::Private(__func__); + + UniquePtr<PendingServiceWorkerOp> op = + MakeUnique<PendingServiceWorkerOp>(std::move(aArgs), promise); + + if (!op->MaybeStart(this)) { + mPendingOps.AppendElement(std::move(op)); + } + + return promise; +} + +RefPtr<ServiceWorkerFetchEventOpPromise> +RemoteWorkerController::ExecServiceWorkerFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<FetchEventOpParent> aReal) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIsServiceWorker); + + RefPtr<ServiceWorkerFetchEventOpPromise::Private> promise = + new ServiceWorkerFetchEventOpPromise::Private(__func__); + + UniquePtr<PendingSWFetchEventOp> op = + MakeUnique<PendingSWFetchEventOp>(aArgs, promise, std::move(aReal)); + + if (!op->MaybeStart(this)) { + mPendingOps.AppendElement(std::move(op)); + } + + return promise; +} + +RefPtr<GenericPromise> RemoteWorkerController::SetServiceWorkerSkipWaitingFlag() + const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mObserver); + + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + static_cast<RemoteWorkerControllerParent*>(mObserver.get()) + ->MaybeSendSetServiceWorkerSkipWaitingFlag( + [promise](bool aOk) { promise->Resolve(aOk, __func__); }); + + return promise; +} + +bool RemoteWorkerController::IsTerminated() const { + return mState == eTerminated; +} + +RemoteWorkerController::PendingSharedWorkerOp::PendingSharedWorkerOp( + Type aType, uint64_t aWindowID) + : mType(aType), mWindowID(aWindowID) { + AssertIsOnBackgroundThread(); +} + +RemoteWorkerController::PendingSharedWorkerOp::PendingSharedWorkerOp( + const MessagePortIdentifier& aPortIdentifier) + : mType(ePortIdentifier), mPortIdentifier(aPortIdentifier) { + AssertIsOnBackgroundThread(); +} + +RemoteWorkerController::PendingSharedWorkerOp::~PendingSharedWorkerOp() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mCompleted); +} + +bool RemoteWorkerController::PendingSharedWorkerOp::MaybeStart( + RemoteWorkerController* const aOwner) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mCompleted); + MOZ_ASSERT(aOwner); + + if (aOwner->mState == RemoteWorkerController::eTerminated) { + Cancel(); + return true; + } + + if (aOwner->mState == RemoteWorkerController::ePending && + mType != eTerminate) { + return false; + } + + switch (mType) { + case eTerminate: + aOwner->Shutdown(); + break; + case eSuspend: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerSuspendOp()); + break; + case eResume: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerResumeOp()); + break; + case eFreeze: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerFreezeOp()); + break; + case eThaw: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerThawOp()); + break; + case ePortIdentifier: + Unused << aOwner->mActor->SendExecOp( + RemoteWorkerPortIdentifierOp(mPortIdentifier)); + break; + case eAddWindowID: + Unused << aOwner->mActor->SendExecOp( + RemoteWorkerAddWindowIDOp(mWindowID)); + break; + case eRemoveWindowID: + Unused << aOwner->mActor->SendExecOp( + RemoteWorkerRemoveWindowIDOp(mWindowID)); + break; + default: + MOZ_CRASH("Unknown op."); + } + + mCompleted = true; + + return true; +} + +void RemoteWorkerController::PendingSharedWorkerOp::Cancel() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mCompleted); + + // We don't want to leak the port if the operation has not been processed. + if (mType == ePortIdentifier) { + MessagePortParent::ForceClose(mPortIdentifier.uuid(), + mPortIdentifier.destinationUuid(), + mPortIdentifier.sequenceId()); + } + + mCompleted = true; +} + +RemoteWorkerController::PendingServiceWorkerOp::PendingServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, + RefPtr<ServiceWorkerOpPromise::Private> aPromise) + : mArgs(std::move(aArgs)), mPromise(std::move(aPromise)) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); +} + +RemoteWorkerController::PendingServiceWorkerOp::~PendingServiceWorkerOp() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(!mPromise); +} + +bool RemoteWorkerController::PendingServiceWorkerOp::MaybeStart( + RemoteWorkerController* const aOwner) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + MOZ_ASSERT(aOwner); + + if (NS_WARN_IF(aOwner->mState == RemoteWorkerController::eTerminated)) { + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; + return true; + } + + // The target content process must still be starting up. + if (!aOwner->mActor) { + // We can avoid starting the worker at all if we know it should be + // terminated. + MOZ_ASSERT(aOwner->mState == RemoteWorkerController::ePending); + if (mArgs.type() == + ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs) { + aOwner->CancelAllPendingOps(); + Cancel(); + + aOwner->mState = RemoteWorkerController::eTerminated; + + return true; + } + + return false; + } + + /** + * Allow termination operations to pass through while pending because the + * remote Service Worker can be terminated while still starting up. + */ + if (aOwner->mState == RemoteWorkerController::ePending && + mArgs.type() != + ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs) { + return false; + } + + MaybeReportServiceWorkerShutdownProgress(mArgs); + + aOwner->mActor->SendExecServiceWorkerOp(mArgs)->Then( + GetCurrentSerialEventTarget(), __func__, + [promise = std::move(mPromise)]( + PRemoteWorkerParent::ExecServiceWorkerOpPromise:: + ResolveOrRejectValue&& aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + return; + } + + promise->Resolve(std::move(aResult.ResolveValue()), __func__); + }); + + return true; +} + +void RemoteWorkerController::PendingServiceWorkerOp::Cancel() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; +} + +RemoteWorkerController::PendingSWFetchEventOp::PendingSWFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<ServiceWorkerFetchEventOpPromise::Private> aPromise, + RefPtr<FetchEventOpParent>&& aReal) + : mArgs(aArgs), mPromise(std::move(aPromise)), mReal(aReal) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + + // If there is a TParentToParentStream in the request body, we need to + // save it to our stream. + IPCInternalRequest& req = mArgs.common().internalRequest(); + if (req.body().isSome() && + req.body().ref().type() == BodyStreamVariant::TParentToParentStream) { + nsCOMPtr<nsIInputStream> stream; + auto streamLength = req.bodySize(); + const auto& uuid = req.body().ref().get_ParentToParentStream().uuid(); + + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + MOZ_DIAGNOSTIC_ASSERT(storage); + storage->GetStream(uuid, 0, streamLength, getter_AddRefs(mBodyStream)); + storage->ForgetStream(uuid); + + MOZ_DIAGNOSTIC_ASSERT(mBodyStream); + + req.body() = Nothing(); + } +} + +RemoteWorkerController::PendingSWFetchEventOp::~PendingSWFetchEventOp() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(!mPromise); +} + +bool RemoteWorkerController::PendingSWFetchEventOp::MaybeStart( + RemoteWorkerController* const aOwner) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + MOZ_ASSERT(aOwner); + + if (NS_WARN_IF(aOwner->mState == RemoteWorkerController::eTerminated)) { + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; + // Because the worker has transitioned to terminated, this operation is moot + // and so we should return true because there's no need to queue it. + return true; + } + + // The target content process must still be starting up. + if (!aOwner->mActor) { + MOZ_ASSERT(aOwner->mState == RemoteWorkerController::ePending); + return false; + } + + // At this point we are handing off responsibility for the promise to the + // actor. + FetchEventOpProxyParent::Create(aOwner->mActor.get(), std::move(mPromise), + mArgs, std::move(mReal), + std::move(mBodyStream)); + + return true; +} + +void RemoteWorkerController::PendingSWFetchEventOp::Cancel() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + + if (mPromise) { + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerController.h b/dom/workers/remoteworkers/RemoteWorkerController.h new file mode 100644 index 0000000000..af53634abd --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerController.h @@ -0,0 +1,323 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerController_h +#define mozilla_dom_RemoteWorkerController_h + +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" +#include "mozilla/dom/ServiceWorkerOpPromise.h" + +namespace mozilla::dom { + +/* Here's a graph about this remote workers are spawned. + * + * _________________________________ | ________________________________ + * | | | | | + * | Parent process | IPC | Creation of Process X | + * | PBackground thread | | | | + * | | | | [RemoteWorkerService::Init()] | + * | | | | | | + * | | | | | (1) | + * | [RemoteWorkerManager:: (2) | | | V | + * | RegisterActor()]<-------- [new RemoteWorkerServiceChild] | + * | | | | | + * | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | | |________________________________| + * | | | + * | new SharedWorker/ServiceWorker | | + * | | ^ | IPC + * | (3) | (4)| | + * | V | | | + * | [RemoteWorkerController:: | | + * | | Create(data)] | | + * | | (5) | | + * | V | | + * | [RemoteWorkerManager::Launch()] | | + * | | | IPC _____________________________ + * | | (6) | | | | + * | | | | Selected content process | + * | V | (7) | | + * | [SendPRemoteWorkerConstructor()]--------->[new RemoteWorkerChild()] | + * | | | | | | | + * | | (8) | | | | | + * | V | | | V | + * | [RemoteWorkerController-> | | | RemoteWorkerChild->Exec() | + * | | SetControllerActor()] | | |_____________________________| + * | (9) | | IPC + * | V | | + * | [RemoteWorkerObserver-> | | + * | CreationCompleted()] | | + * |_________________________________| | + * | + * + * 1. When a new process starts, it creates a RemoteWorkerService singleton. + * This service creates a new thread (Worker Launcher) and from there, it + * starts a PBackground RemoteWorkerServiceChild actor. + * 2. On the parent process, PBackground thread, RemoteWorkerServiceParent + * actors are registered into the RemoteWorkerManager service. + * + * 3. At some point, a SharedWorker or a ServiceWorker must be executed. + * RemoteWorkerController::Create() is used to start the launching. This + * method must be called on the parent process, on the PBackground thread. + * 4. RemoteWorkerController object is immediately returned to the caller. Any + * operation done with this controller object will be stored in a queue, + * until the launching is correctly executed. + * 5. RemoteWorkerManager has the list of active RemoteWorkerServiceParent + * actors. From them, it picks one. + * In case we don't have any content process to select, a new one is + * spawned. If this happens, the operation is suspended until a new + * RemoteWorkerServiceParent is registered. + * 6. RemoteWorkerServiceParent is used to create a RemoteWorkerParent. + * 7. RemoteWorkerChild is created on a selected process and it executes the + * WorkerPrivate. + * 8. The RemoteWorkerParent actor is passed to the RemoteWorkerController. + * 9. RemoteWorkerController now is ready to continue and it called + * RemoteWorkerObserver to inform that the operation is completed. + * In case there were pending operations, they are now executed. + */ + +class ErrorValue; +class FetchEventOpParent; +class RemoteWorkerControllerParent; +class RemoteWorkerData; +class RemoteWorkerManager; +class RemoteWorkerParent; + +class RemoteWorkerObserver { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void CreationFailed() = 0; + + virtual void CreationSucceeded() = 0; + + virtual void ErrorReceived(const ErrorValue& aValue) = 0; + + virtual void LockNotified(bool aCreated) = 0; + + virtual void WebTransportNotified(bool aCreated) = 0; + + virtual void Terminated() = 0; +}; + +/** + * PBackground instance created by static RemoteWorkerController::Create that + * builds on RemoteWorkerManager. Interface to control the remote worker as well + * as receive events via the RemoteWorkerObserver interface that the owner + * (SharedWorkerManager in this case) must implement to hear about errors, + * termination, and whether the initial spawning succeeded/failed. + * + * Its methods may be called immediately after creation even though the worker + * is created asynchronously; an internal operation queue makes this work. + * Communicates with the remote worker via owned RemoteWorkerParent over + * PRemoteWorker protocol. + */ +class RemoteWorkerController final { + friend class RemoteWorkerControllerParent; + friend class RemoteWorkerManager; + friend class RemoteWorkerParent; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerController) + + static already_AddRefed<RemoteWorkerController> Create( + const RemoteWorkerData& aData, RemoteWorkerObserver* aObserver, + base::ProcessId = 0); + + void AddWindowID(uint64_t aWindowID); + + void RemoveWindowID(uint64_t aWindowID); + + void AddPortIdentifier(const MessagePortIdentifier& aPortIdentifier); + + void Terminate(); + + void Suspend(); + + void Resume(); + + void Freeze(); + + void Thaw(); + + RefPtr<ServiceWorkerOpPromise> ExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs); + + RefPtr<ServiceWorkerFetchEventOpPromise> ExecServiceWorkerFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<FetchEventOpParent> aReal); + + RefPtr<GenericPromise> SetServiceWorkerSkipWaitingFlag() const; + + bool IsTerminated() const; + + void NotifyWebTransport(bool aCreated); + + private: + RemoteWorkerController(const RemoteWorkerData& aData, + RemoteWorkerObserver* aObserver); + + ~RemoteWorkerController(); + + void SetWorkerActor(RemoteWorkerParent* aActor); + + void NoteDeadWorkerActor(); + + void ErrorPropagation(const ErrorValue& aValue); + + void NotifyLock(bool aCreated); + + void WorkerTerminated(); + + void Shutdown(); + + void CreationFailed(); + + void CreationSucceeded(); + + void CancelAllPendingOps(); + + template <typename... Args> + void MaybeStartSharedWorkerOp(Args&&... aArgs); + + void NoteDeadWorker(); + + RefPtr<RemoteWorkerObserver> mObserver; + RefPtr<RemoteWorkerParent> mActor; + + enum { + ePending, + eReady, + eTerminated, + } mState; + + const bool mIsServiceWorker; + + /** + * `PendingOp` is responsible for encapsulating logic for starting and + * canceling pending remote worker operations, as this logic may vary + * depending on the type of the remote worker and the type of the operation. + */ + class PendingOp { + public: + PendingOp() = default; + + PendingOp(const PendingOp&) = delete; + + PendingOp& operator=(const PendingOp&) = delete; + + virtual ~PendingOp() = default; + + /** + * Returns `true` if execution has started or the operation is moot and + * doesn't need to be queued, `false` if execution hasn't started and the + * operation should be queued. In general, operations should only return + * false when a remote worker is first starting up. Operations may also + * somewhat non-intuitively return true without doing anything if the worker + * has already been told to shutdown. + * + * Starting execution may depend the state of `aOwner.` + */ + virtual bool MaybeStart(RemoteWorkerController* const aOwner) = 0; + + /** + * Invoked if the operation will never have MaybeStart() called again + * because the RemoteWorkerController has terminated (or will never start). + * This should be used by PendingOps to clean up any resources they own and + * may also be called internally by their MaybeStart() methods if they + * determine the worker has been terminated. This should be idempotent. + */ + virtual void Cancel() = 0; + }; + + class PendingSharedWorkerOp final : public PendingOp { + public: + enum Type { + eTerminate, + eSuspend, + eResume, + eFreeze, + eThaw, + ePortIdentifier, + eAddWindowID, + eRemoveWindowID, + }; + + explicit PendingSharedWorkerOp(Type aType, uint64_t aWindowID = 0); + + explicit PendingSharedWorkerOp( + const MessagePortIdentifier& aPortIdentifier); + + ~PendingSharedWorkerOp(); + + bool MaybeStart(RemoteWorkerController* const aOwner) override; + + void Cancel() override; + + private: + const Type mType; + const MessagePortIdentifier mPortIdentifier; + const uint64_t mWindowID = 0; + bool mCompleted = false; + }; + + class PendingServiceWorkerOp final : public PendingOp { + public: + PendingServiceWorkerOp(ServiceWorkerOpArgs&& aArgs, + RefPtr<ServiceWorkerOpPromise::Private> aPromise); + + ~PendingServiceWorkerOp(); + + bool MaybeStart(RemoteWorkerController* const aOwner) override; + + void Cancel() override; + + private: + ServiceWorkerOpArgs mArgs; + RefPtr<ServiceWorkerOpPromise::Private> mPromise; + }; + + /** + * Custom pending op type to deal with the complexities of FetchEvents having + * their own actor. + * + * FetchEvent Ops have their own actor type because their lifecycle is more + * complex than IPDL's async return value mechanism allows. Additionally, + * its IPC struct potentially has to serialize RemoteLazyStreams which + * requires us to hold an nsIInputStream when at rest and serialize it when + * eventually sending. + */ + class PendingSWFetchEventOp final : public PendingOp { + public: + PendingSWFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<ServiceWorkerFetchEventOpPromise::Private> aPromise, + RefPtr<FetchEventOpParent>&& aReal); + + ~PendingSWFetchEventOp(); + + bool MaybeStart(RemoteWorkerController* const aOwner) override; + + void Cancel() override; + + private: + ParentToParentServiceWorkerFetchEventOpArgs mArgs; + RefPtr<ServiceWorkerFetchEventOpPromise::Private> mPromise; + RefPtr<FetchEventOpParent> mReal; + nsCOMPtr<nsIInputStream> mBodyStream; + }; + + nsTArray<UniquePtr<PendingOp>> mPendingOps; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerController_h diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerChild.cpp b/dom/workers/remoteworkers/RemoteWorkerControllerChild.cpp new file mode 100644 index 0000000000..5a1db77cf0 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerChild.cpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerControllerChild.h" + +#include <utility> + +#include "MainThreadUtils.h" +#include "nsError.h" +#include "nsThreadUtils.h" + +#include "ServiceWorkerPrivate.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/PFetchEventOpChild.h" + +namespace mozilla { + +using ipc::IPCResult; + +namespace dom { + +RemoteWorkerControllerChild::RemoteWorkerControllerChild( + RefPtr<RemoteWorkerObserver> aObserver) + : mObserver(std::move(aObserver)) { + AssertIsOnMainThread(); + mRemoteWorkerLaunchStart = TimeStamp::Now(); + MOZ_ASSERT(mObserver); +} + +PFetchEventOpChild* RemoteWorkerControllerChild::AllocPFetchEventOpChild( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) { + MOZ_CRASH("PFetchEventOpChild actors must be manually constructed!"); + return nullptr; +} + +TimeStamp RemoteWorkerControllerChild::GetRemoteWorkerLaunchStart() { + MOZ_ASSERT(mRemoteWorkerLaunchStart); + return mRemoteWorkerLaunchStart; +} + +TimeStamp RemoteWorkerControllerChild::GetRemoteWorkerLaunchEnd() { + MOZ_ASSERT(mRemoteWorkerLaunchEnd); + return mRemoteWorkerLaunchEnd; +} + +bool RemoteWorkerControllerChild::DeallocPFetchEventOpChild( + PFetchEventOpChild* aActor) { + AssertIsOnMainThread(); + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +void RemoteWorkerControllerChild::ActorDestroy(ActorDestroyReason aReason) { + AssertIsOnMainThread(); + + mIPCActive = false; + + if (NS_WARN_IF(mObserver)) { + mObserver->ErrorReceived(NS_ERROR_DOM_ABORT_ERR); + } +} + +IPCResult RemoteWorkerControllerChild::RecvCreationFailed() { + AssertIsOnMainThread(); + + if (mObserver) { + mObserver->CreationFailed(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvCreationSucceeded() { + AssertIsOnMainThread(); + mRemoteWorkerLaunchEnd = TimeStamp::Now(); + + if (mObserver) { + mObserver->CreationSucceeded(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvErrorReceived( + const ErrorValue& aError) { + AssertIsOnMainThread(); + mRemoteWorkerLaunchEnd = TimeStamp::Now(); + + if (mObserver) { + mObserver->ErrorReceived(aError); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvTerminated() { + AssertIsOnMainThread(); + + if (mObserver) { + mObserver->Terminated(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve) { + AssertIsOnMainThread(); + + if (mObserver) { + static_cast<ServiceWorkerPrivate*>(mObserver.get()) + ->SetSkipWaitingFlag() + ->Then(GetCurrentSerialEventTarget(), __func__, + [resolve = std::move(aResolve)]( + const GenericPromise::ResolveOrRejectValue& aResult) { + resolve(aResult.IsResolve() ? aResult.ResolveValue() : false); + }); + + return IPC_OK(); + } + + aResolve(false); + + return IPC_OK(); +} + +void RemoteWorkerControllerChild::RevokeObserver( + RemoteWorkerObserver* aObserver) { + AssertIsOnMainThread(); + MOZ_ASSERT(aObserver); + MOZ_ASSERT(aObserver == mObserver); + + mObserver = nullptr; +} + +void RemoteWorkerControllerChild::MaybeSendDelete() { + AssertIsOnMainThread(); + + if (!mIPCActive) { + return; + } + + RefPtr<RemoteWorkerControllerChild> self = this; + + SendShutdown()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = std::move(self)](const ShutdownPromise::ResolveOrRejectValue&) { + if (self->mIPCActive) { + Unused << self->Send__delete__(self); + } + }); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerChild.h b/dom/workers/remoteworkers/RemoteWorkerControllerChild.h new file mode 100644 index 0000000000..283fff3916 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerChild.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_remoteworkercontrollerchild_h__ +#define mozilla_dom_remoteworkercontrollerchild_h__ + +#include "nsISupportsImpl.h" + +#include "RemoteWorkerController.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/PRemoteWorkerControllerChild.h" + +namespace mozilla::dom { + +/** + * Parent-process main-thread proxy used by ServiceWorkerManager to control + * RemoteWorkerController instances on the parent-process PBackground thread. + */ +class RemoteWorkerControllerChild final : public PRemoteWorkerControllerChild { + friend class PRemoteWorkerControllerChild; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerControllerChild, override) + + explicit RemoteWorkerControllerChild(RefPtr<RemoteWorkerObserver> aObserver); + + void Initialize(); + + void RevokeObserver(RemoteWorkerObserver* aObserver); + + void MaybeSendDelete(); + + TimeStamp GetRemoteWorkerLaunchStart(); + TimeStamp GetRemoteWorkerLaunchEnd(); + + private: + ~RemoteWorkerControllerChild() = default; + + PFetchEventOpChild* AllocPFetchEventOpChild( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs); + + bool DeallocPFetchEventOpChild(PFetchEventOpChild* aActor); + + void ActorDestroy(ActorDestroyReason aReason) override; + + mozilla::ipc::IPCResult RecvCreationFailed(); + + mozilla::ipc::IPCResult RecvCreationSucceeded(); + + mozilla::ipc::IPCResult RecvErrorReceived(const ErrorValue& aError); + + mozilla::ipc::IPCResult RecvTerminated(); + + mozilla::ipc::IPCResult RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve); + + RefPtr<RemoteWorkerObserver> mObserver; + + bool mIPCActive = true; + + TimeStamp mRemoteWorkerLaunchStart; + TimeStamp mRemoteWorkerLaunchEnd; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_remoteworkercontrollerchild_h__ diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp b/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp new file mode 100644 index 0000000000..7ac901f655 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp @@ -0,0 +1,215 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerControllerParent.h" + +#include <utility> + +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsThreadUtils.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/FetchEventOpParent.h" +#include "mozilla/dom/RemoteWorkerParent.h" +#include "mozilla/dom/ServiceWorkerOpPromise.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +RemoteWorkerControllerParent::RemoteWorkerControllerParent( + const RemoteWorkerData& aRemoteWorkerData) + : mRemoteWorkerController(RemoteWorkerController::Create( + aRemoteWorkerData, this, 0 /* random process ID */)) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mRemoteWorkerController); +} + +RefPtr<RemoteWorkerParent> RemoteWorkerControllerParent::GetRemoteWorkerParent() + const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mRemoteWorkerController); + + return mRemoteWorkerController->mActor; +} + +void RemoteWorkerControllerParent::MaybeSendSetServiceWorkerSkipWaitingFlag( + std::function<void(bool)>&& aCallback) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aCallback); + + if (!mIPCActive) { + aCallback(false); + return; + } + + SendSetServiceWorkerSkipWaitingFlag()->Then( + GetCurrentSerialEventTarget(), __func__, + [callback = std::move(aCallback)]( + const SetServiceWorkerSkipWaitingFlagPromise::ResolveOrRejectValue& + aResult) { + callback(aResult.IsResolve() ? aResult.ResolveValue() : false); + }); +} + +RemoteWorkerControllerParent::~RemoteWorkerControllerParent() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mIPCActive); + MOZ_ASSERT(!mRemoteWorkerController); +} + +PFetchEventOpParent* RemoteWorkerControllerParent::AllocPFetchEventOpParent( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) { + AssertIsOnBackgroundThread(); + + RefPtr<FetchEventOpParent> actor = new FetchEventOpParent(); + return actor.forget().take(); +} + +IPCResult RemoteWorkerControllerParent::RecvPFetchEventOpConstructor( + PFetchEventOpParent* aActor, + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<FetchEventOpParent> realFetchOp = + static_cast<FetchEventOpParent*>(aActor); + mRemoteWorkerController->ExecServiceWorkerFetchEventOp(aArgs, realFetchOp) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [fetchOp = std::move(realFetchOp)]( + ServiceWorkerFetchEventOpPromise::ResolveOrRejectValue&& + aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + MOZ_ASSERT(NS_FAILED(aResult.RejectValue())); + Unused << fetchOp->Send__delete__(fetchOp, aResult.RejectValue()); + return; + } + + Unused << fetchOp->Send__delete__(fetchOp, aResult.ResolveValue()); + }); + + return IPC_OK(); +} + +bool RemoteWorkerControllerParent::DeallocPFetchEventOpParent( + PFetchEventOpParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<FetchEventOpParent> actor = + dont_AddRef(static_cast<FetchEventOpParent*>(aActor)); + return true; +} + +IPCResult RemoteWorkerControllerParent::RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIPCActive); + MOZ_ASSERT(mRemoteWorkerController); + + mRemoteWorkerController->ExecServiceWorkerOp(std::move(aArgs)) + ->Then(GetCurrentSerialEventTarget(), __func__, + [resolve = std::move(aResolve)]( + ServiceWorkerOpPromise::ResolveOrRejectValue&& aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + MOZ_ASSERT(NS_FAILED(aResult.RejectValue())); + resolve(aResult.RejectValue()); + return; + } + + resolve(aResult.ResolveValue()); + }); + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerParent::RecvShutdown( + ShutdownResolver&& aResolve) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIPCActive); + MOZ_ASSERT(mRemoteWorkerController); + + mIPCActive = false; + + mRemoteWorkerController->Shutdown(); + mRemoteWorkerController = nullptr; + + aResolve(true); + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerParent::Recv__delete__() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mIPCActive); + MOZ_ASSERT(!mRemoteWorkerController); + + return IPC_OK(); +} + +void RemoteWorkerControllerParent::ActorDestroy(ActorDestroyReason aReason) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mIPCActive)) { + mIPCActive = false; + } + + if (NS_WARN_IF(mRemoteWorkerController)) { + mRemoteWorkerController->Shutdown(); + mRemoteWorkerController = nullptr; + } +} + +void RemoteWorkerControllerParent::CreationFailed() { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendCreationFailed(); +} + +void RemoteWorkerControllerParent::CreationSucceeded() { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendCreationSucceeded(); +} + +void RemoteWorkerControllerParent::ErrorReceived(const ErrorValue& aValue) { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendErrorReceived(aValue); +} + +void RemoteWorkerControllerParent::Terminated() { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendTerminated(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerParent.h b/dom/workers/remoteworkers/RemoteWorkerControllerParent.h new file mode 100644 index 0000000000..618fbf9506 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerParent.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_remoteworkercontrollerparent_h__ +#define mozilla_dom_remoteworkercontrollerparent_h__ + +#include <functional> + +#include "nsISupportsImpl.h" + +#include "RemoteWorkerController.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/PRemoteWorkerControllerParent.h" + +namespace mozilla::dom { + +/** + * PBackground-resident proxy used by ServiceWorkerManager because canonical + * ServiceWorkerManager state exists on the parent process main thread but the + * RemoteWorkerController API is used from the parent process PBackground + * thread. + */ +class RemoteWorkerControllerParent final : public PRemoteWorkerControllerParent, + public RemoteWorkerObserver { + friend class PRemoteWorkerControllerParent; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerControllerParent, override) + + explicit RemoteWorkerControllerParent( + const RemoteWorkerData& aRemoteWorkerData); + + // Returns the corresponding RemoteWorkerParent (if any). + RefPtr<RemoteWorkerParent> GetRemoteWorkerParent() const; + + void MaybeSendSetServiceWorkerSkipWaitingFlag( + std::function<void(bool)>&& aCallback); + + private: + ~RemoteWorkerControllerParent(); + + PFetchEventOpParent* AllocPFetchEventOpParent( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs); + + mozilla::ipc::IPCResult RecvPFetchEventOpConstructor( + PFetchEventOpParent* aActor, + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) override; + + bool DeallocPFetchEventOpParent(PFetchEventOpParent* aActor); + + mozilla::ipc::IPCResult RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve); + + mozilla::ipc::IPCResult RecvShutdown(ShutdownResolver&& aResolve); + + mozilla::ipc::IPCResult Recv__delete__() override; + + void ActorDestroy(ActorDestroyReason aReason) override; + + void CreationFailed() override; + + void CreationSucceeded() override; + + void ErrorReceived(const ErrorValue& aValue) override; + + void LockNotified(bool aCreated) final { + // no-op for service workers + } + + void WebTransportNotified(bool aCreated) final { + // no-op for service workers + } + + void Terminated() override; + + RefPtr<RemoteWorkerController> mRemoteWorkerController; + + bool mIPCActive = true; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_remoteworkercontrollerparent_h__ diff --git a/dom/workers/remoteworkers/RemoteWorkerManager.cpp b/dom/workers/remoteworkers/RemoteWorkerManager.cpp new file mode 100644 index 0000000000..29577f0a34 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerManager.cpp @@ -0,0 +1,627 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerManager.h" + +#include <utility> + +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/ContentChild.h" // ContentChild::GetSingleton +#include "mozilla/dom/ProcessIsolation.h" +#include "mozilla/dom/RemoteWorkerController.h" +#include "mozilla/dom/RemoteWorkerParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "nsCOMPtr.h" +#include "nsImportModule.h" +#include "nsIXULRuntime.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "RemoteWorkerServiceParent.h" + +mozilla::LazyLogModule gRemoteWorkerManagerLog("RemoteWorkerManager"); + +#ifdef LOG +# undef LOG +#endif +#define LOG(fmt) \ + MOZ_LOG(gRemoteWorkerManagerLog, mozilla::LogLevel::Verbose, fmt) + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +// Raw pointer because this object is kept alive by RemoteWorkerServiceParent +// actors. +RemoteWorkerManager* sRemoteWorkerManager; + +bool IsServiceWorker(const RemoteWorkerData& aData) { + return aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData; +} + +void TransmitPermissionsAndBlobURLsForPrincipalInfo( + ContentParent* aContentParent, const PrincipalInfo& aPrincipalInfo) { + AssertIsOnMainThread(); + MOZ_ASSERT(aContentParent); + + auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo); + + if (NS_WARN_IF(principalOrErr.isErr())) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + aContentParent->TransmitBlobURLsForPrincipal(principal); + + MOZ_ALWAYS_SUCCEEDS( + aContentParent->TransmitPermissionsForPrincipal(principal)); +} + +} // namespace + +// static +bool RemoteWorkerManager::MatchRemoteType(const nsACString& processRemoteType, + const nsACString& workerRemoteType) { + LOG(("MatchRemoteType [processRemoteType=%s, workerRemoteType=%s]", + PromiseFlatCString(processRemoteType).get(), + PromiseFlatCString(workerRemoteType).get())); + + // Respecting COOP and COEP requires processing headers in the parent + // process in order to choose an appropriate content process, but the + // workers' ScriptLoader processes headers in content processes. An + // intermediary step that provides security guarantees is to simply never + // allow SharedWorkers and ServiceWorkers to exist in a COOP+COEP process. + // The ultimate goal is to allow these worker types to be put in such + // processes based on their script response headers. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1595206 + // + // RemoteWorkerManager::GetRemoteType should not select this remoteType + // and so workerRemoteType is not expected to be set to a coop+coep + // remoteType and here we can just assert that it is not happening. + MOZ_ASSERT(!IsWebCoopCoepRemoteType(workerRemoteType)); + + return processRemoteType.Equals(workerRemoteType); +} + +// static +Result<nsCString, nsresult> RemoteWorkerManager::GetRemoteType( + const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind) { + AssertIsOnMainThread(); + + MOZ_ASSERT_IF(aWorkerKind == WorkerKind::WorkerKindService, + aPrincipal->GetIsContentPrincipal()); + + // If E10S is fully disabled, there are no decisions to be made, and we need + // to finish the load in the parent process. + if (!BrowserTabsRemoteAutostart()) { + LOG(("GetRemoteType: Loading in parent process as e10s is disabled")); + return NOT_REMOTE_TYPE; + } + + nsCString preferredRemoteType = DEFAULT_REMOTE_TYPE; + if (aWorkerKind == WorkerKind::WorkerKindShared) { + if (auto* contentChild = ContentChild::GetSingleton()) { + // For a shared worker set the preferred remote type to the content + // child process remote type. + preferredRemoteType = contentChild->GetRemoteType(); + } else if (aPrincipal->IsSystemPrincipal()) { + preferredRemoteType = NOT_REMOTE_TYPE; + } + } + + auto result = IsolationOptionsForWorker( + aPrincipal, aWorkerKind, preferredRemoteType, FissionAutostart()); + if (NS_WARN_IF(result.isErr())) { + LOG(("GetRemoteType Abort: IsolationOptionsForWorker failed")); + return Err(NS_ERROR_DOM_ABORT_ERR); + } + auto options = result.unwrap(); + + if (MOZ_LOG_TEST(gRemoteWorkerManagerLog, LogLevel::Verbose)) { + nsCString principalOrigin; + aPrincipal->GetOrigin(principalOrigin); + + LOG( + ("GetRemoteType workerType=%s, principal=%s, " + "preferredRemoteType=%s, selectedRemoteType=%s", + aWorkerKind == WorkerKind::WorkerKindService ? "service" : "shared", + principalOrigin.get(), preferredRemoteType.get(), + options.mRemoteType.get())); + } + + return options.mRemoteType; +} + +// static +bool RemoteWorkerManager::HasExtensionPrincipal(const RemoteWorkerData& aData) { + auto principalInfo = aData.principalInfo(); + return principalInfo.type() == PrincipalInfo::TContentPrincipalInfo && + // This helper method is also called from the background thread and so + // we can't check if the principal does have an addonPolicy object + // associated and we have to resort to check the url scheme instead. + StringBeginsWith(principalInfo.get_ContentPrincipalInfo().spec(), + "moz-extension://"_ns); +} + +// static +bool RemoteWorkerManager::IsRemoteTypeAllowed(const RemoteWorkerData& aData) { + AssertIsOnMainThread(); + + // If Gecko is running in single process mode, there is no child process + // to select and we have to just consider it valid (if it should haven't + // been launched it should have been already prevented before reaching + // a RemoteWorkerChild instance). + if (!BrowserTabsRemoteAutostart()) { + return true; + } + + const auto& principalInfo = aData.principalInfo(); + + auto* contentChild = ContentChild::GetSingleton(); + if (!contentChild) { + // If e10s isn't disabled, only workers related to the system principal + // should be allowed to run in the parent process, and extension principals + // if extensions.webextensions.remote is false. + return principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || + (!StaticPrefs::extensions_webextensions_remote() && + aData.remoteType().Equals(NOT_REMOTE_TYPE) && + HasExtensionPrincipal(aData)); + } + + auto principalOrErr = PrincipalInfoToPrincipal(principalInfo); + if (NS_WARN_IF(principalOrErr.isErr())) { + return false; + } + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + // Recompute the remoteType based on the principal, to double-check that it + // has not been tempered to select a different child process than the one + // expected. + bool isServiceWorker = aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData; + auto remoteType = GetRemoteType( + principal, isServiceWorker ? WorkerKindService : WorkerKindShared); + if (NS_WARN_IF(remoteType.isErr())) { + LOG(("IsRemoteTypeAllowed: Error to retrieve remote type")); + return false; + } + + return MatchRemoteType(remoteType.unwrap(), contentChild->GetRemoteType()); +} + +/* static */ +already_AddRefed<RemoteWorkerManager> RemoteWorkerManager::GetOrCreate() { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + if (!sRemoteWorkerManager) { + sRemoteWorkerManager = new RemoteWorkerManager(); + } + + RefPtr<RemoteWorkerManager> rwm = sRemoteWorkerManager; + return rwm.forget(); +} + +RemoteWorkerManager::RemoteWorkerManager() : mParentActor(nullptr) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!sRemoteWorkerManager); +} + +RemoteWorkerManager::~RemoteWorkerManager() { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(sRemoteWorkerManager == this); + sRemoteWorkerManager = nullptr; +} + +void RemoteWorkerManager::RegisterActor(RemoteWorkerServiceParent* aActor) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (!BackgroundParent::IsOtherProcessActor(aActor->Manager())) { + MOZ_ASSERT(!mParentActor); + mParentActor = aActor; + MOZ_ASSERT(mPendings.IsEmpty()); + return; + } + + MOZ_ASSERT(!mChildActors.Contains(aActor)); + mChildActors.AppendElement(aActor); + + if (!mPendings.IsEmpty()) { + const auto& processRemoteType = aActor->GetRemoteType(); + nsTArray<Pending> unlaunched; + + // Flush pending launching. + for (Pending& p : mPendings) { + if (p.mController->IsTerminated()) { + continue; + } + + const auto& workerRemoteType = p.mData.remoteType(); + + if (MatchRemoteType(processRemoteType, workerRemoteType)) { + LOG(("RegisterActor - Launch Pending, workerRemoteType=%s", + workerRemoteType.get())); + LaunchInternal(p.mController, aActor, p.mData); + } else { + unlaunched.AppendElement(std::move(p)); + continue; + } + } + + std::swap(mPendings, unlaunched); + + // AddRef is called when the first Pending object is added to mPendings, so + // the balancing Release is called when the last Pending object is removed. + // RemoteWorkerServiceParents will hold strong references to + // RemoteWorkerManager. + if (mPendings.IsEmpty()) { + Release(); + } + + LOG(("RegisterActor - mPendings length: %zu", mPendings.Length())); + } +} + +void RemoteWorkerManager::UnregisterActor(RemoteWorkerServiceParent* aActor) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (aActor == mParentActor) { + mParentActor = nullptr; + } else { + MOZ_ASSERT(mChildActors.Contains(aActor)); + mChildActors.RemoveElement(aActor); + } +} + +void RemoteWorkerManager::Launch(RemoteWorkerController* aController, + const RemoteWorkerData& aData, + base::ProcessId aProcessId) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + RemoteWorkerServiceParent* targetActor = SelectTargetActor(aData, aProcessId); + + // If there is not an available actor, let's store the data, and let's spawn a + // new process. + if (!targetActor) { + // If this is the first time we have a pending launching, we must keep alive + // the manager. + if (mPendings.IsEmpty()) { + AddRef(); + } + + Pending* pending = mPendings.AppendElement(); + pending->mController = aController; + pending->mData = aData; + + // Launching is async, so we cannot check for failures right here. + LaunchNewContentProcess(aData); + return; + } + + /** + * If a target actor for the remote worker has been selected, the actor has + * already been registered with the corresponding `ContentParent` and we + * should not increment the `mRemoteWorkerActorData`'s `mCount` again (see + * `SelectTargetActorForServiceWorker()` / + * `SelectTargetActorForSharedWorker()`). + */ + LaunchInternal(aController, targetActor, aData, true); +} + +void RemoteWorkerManager::LaunchInternal( + RemoteWorkerController* aController, + RemoteWorkerServiceParent* aTargetActor, const RemoteWorkerData& aData, + bool aRemoteWorkerAlreadyRegistered) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aController); + MOZ_ASSERT(aTargetActor); + MOZ_ASSERT(aTargetActor == mParentActor || + mChildActors.Contains(aTargetActor)); + + // We need to send permissions to content processes, but not if we're spawning + // the worker here in the parent process. + if (aTargetActor != mParentActor) { + RefPtr<ThreadsafeContentParentHandle> contentHandle = + BackgroundParent::GetContentParentHandle(aTargetActor->Manager()); + + // This won't cause any race conditions because the content process + // should wait for the permissions to be received before executing the + // Service Worker. + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [contentHandle = std::move(contentHandle), + principalInfo = aData.principalInfo()] { + AssertIsOnMainThread(); + if (RefPtr<ContentParent> contentParent = + contentHandle->GetContentParent()) { + TransmitPermissionsAndBlobURLsForPrincipalInfo(contentParent, + principalInfo); + } + }); + + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); + } + + RefPtr<RemoteWorkerParent> workerActor = MakeAndAddRef<RemoteWorkerParent>(); + if (!aTargetActor->Manager()->SendPRemoteWorkerConstructor(workerActor, + aData)) { + AsyncCreationFailed(aController); + return; + } + + workerActor->Initialize(aRemoteWorkerAlreadyRegistered); + + // This makes the link better the 2 actors. + aController->SetWorkerActor(workerActor); + workerActor->SetController(aController); +} + +void RemoteWorkerManager::AsyncCreationFailed( + RemoteWorkerController* aController) { + RefPtr<RemoteWorkerController> controller = aController; + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction("RemoteWorkerManager::AsyncCreationFailed", + [controller]() { controller->CreationFailed(); }); + + NS_DispatchToCurrentThread(r.forget()); +} + +template <typename Callback> +void RemoteWorkerManager::ForEachActor( + Callback&& aCallback, const nsACString& aRemoteType, + Maybe<base::ProcessId> aProcessId) const { + AssertIsOnBackgroundThread(); + + const auto length = mChildActors.Length(); + + auto end = static_cast<uint32_t>(rand()) % length; + if (aProcessId) { + // Start from the actor with the given processId instead of starting from + // a random index. + for (auto j = length - 1; j > 0; j--) { + if (mChildActors[j]->OtherPid() == *aProcessId) { + end = j; + break; + } + } + } + + uint32_t i = end; + + do { + MOZ_ASSERT(i < mChildActors.Length()); + RemoteWorkerServiceParent* actor = mChildActors[i]; + + if (MatchRemoteType(actor->GetRemoteType(), aRemoteType)) { + ThreadsafeContentParentHandle* contentHandle = + BackgroundParent::GetContentParentHandle(actor->Manager()); + + if (!aCallback(actor, contentHandle)) { + break; + } + } + + i = (i + 1) % length; + } while (i != end); +} + +/** + * When selecting a target actor for a given remote worker, we have to consider + * that: + * + * - Service Workers can spawn even when their registering page/script isn't + * active (e.g. push notifications), so we don't attempt to spawn the worker + * in its registering script's process. We search linearly and choose the + * search's starting position randomly. + * + * - When Fission is enabled, Shared Workers may have to be spawned into + * different child process from the one where it has been registered from, and + * that child process may be going to be marked as dead and shutdown. + * + * Spawning the workers in a random process makes the process selection criteria + * a little tricky, as a candidate process may imminently shutdown due to a + * remove worker actor unregistering + * (see `ContentParent::UnregisterRemoveWorkerActor`). + * + * In `ContentParent::MaybeBeginShutdown` we only dispatch a runnable + * to call `ContentParent::ShutDownProcess` if there are no registered remote + * worker actors, and we ensure that the check for the number of registered + * actors and the dispatching of the runnable are atomic. That happens on the + * main thread, so here on the background thread, while + * `ContentParent::mRemoteWorkerActorData` is locked, if `mCount` > 0, we can + * register a remote worker actor "early" and guarantee that the corresponding + * content process will not shutdown. + */ +RemoteWorkerServiceParent* RemoteWorkerManager::SelectTargetActorInternal( + const RemoteWorkerData& aData, base::ProcessId aProcessId) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mChildActors.IsEmpty()); + + RemoteWorkerServiceParent* actor = nullptr; + + const auto& workerRemoteType = aData.remoteType(); + + ForEachActor( + [&](RemoteWorkerServiceParent* aActor, + ThreadsafeContentParentHandle* aContentHandle) { + // Make sure to choose an actor related to a child process that is not + // going to shutdown while we are still in the process of launching the + // remote worker. + // + // ForEachActor will start from the child actor coming from the child + // process with a pid equal to aProcessId if any, otherwise it would + // start from a random actor in the mChildActors array, this guarantees + // that we will choose that actor if it does also match the remote type. + if (aContentHandle->MaybeRegisterRemoteWorkerActor( + [&](uint32_t count, bool shutdownStarted) -> bool { + return (count || !shutdownStarted) && + (aActor->OtherPid() == aProcessId || !actor); + })) { + actor = aActor; + return false; + } + MOZ_ASSERT(!actor); + return true; + }, + workerRemoteType, IsServiceWorker(aData) ? Nothing() : Some(aProcessId)); + + return actor; +} + +RemoteWorkerServiceParent* RemoteWorkerManager::SelectTargetActor( + const RemoteWorkerData& aData, base::ProcessId aProcessId) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + // System principal workers should run on the parent process. + if (aData.principalInfo().type() == PrincipalInfo::TSystemPrincipalInfo) { + MOZ_ASSERT(mParentActor); + return mParentActor; + } + + // Extension principal workers are allowed to run on the parent process + // when "extensions.webextensions.remote" pref is false. + if (aProcessId == base::GetCurrentProcId() && + aData.remoteType().Equals(NOT_REMOTE_TYPE) && + !StaticPrefs::extensions_webextensions_remote() && + HasExtensionPrincipal(aData)) { + MOZ_ASSERT(mParentActor); + return mParentActor; + } + + // If e10s is off, use the parent process. + if (!BrowserTabsRemoteAutostart()) { + MOZ_ASSERT(mParentActor); + return mParentActor; + } + + // We shouldn't have to worry about content-principal parent-process workers. + MOZ_ASSERT(aProcessId != base::GetCurrentProcId()); + + if (mChildActors.IsEmpty()) { + return nullptr; + } + + return SelectTargetActorInternal(aData, aProcessId); +} + +void RemoteWorkerManager::LaunchNewContentProcess( + const RemoteWorkerData& aData) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + nsCOMPtr<nsISerialEventTarget> bgEventTarget = GetCurrentSerialEventTarget(); + + using LaunchPromiseType = ContentParent::LaunchPromise; + using CallbackParamType = LaunchPromiseType::ResolveOrRejectValue; + + // A new content process must be requested on the main thread. On success, + // the success callback will also run on the main thread. On failure, however, + // the failure callback must be run on the background thread - it uses + // RemoteWorkerManager, and RemoteWorkerManager isn't threadsafe, so the + // promise callback will just dispatch the "real" failure callback to the + // background thread. + auto processLaunchCallback = [principalInfo = aData.principalInfo(), + bgEventTarget = std::move(bgEventTarget), + self = RefPtr<RemoteWorkerManager>(this)]( + const CallbackParamType& aValue, + const nsCString& remoteType) mutable { + if (aValue.IsResolve()) { + LOG(("LaunchNewContentProcess: successfully got child process")); + + // The failure callback won't run, and we're on the main thread, so + // we need to properly release the thread-unsafe RemoteWorkerManager. + NS_ProxyRelease(__func__, bgEventTarget, self.forget()); + } else { + // The "real" failure callback. + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), remoteType] { + nsTArray<Pending> uncancelled; + auto pendings = std::move(self->mPendings); + + for (const auto& pending : pendings) { + const auto& workerRemoteType = pending.mData.remoteType(); + if (self->MatchRemoteType(remoteType, workerRemoteType)) { + LOG( + ("LaunchNewContentProcess: Cancel pending with " + "workerRemoteType=%s", + workerRemoteType.get())); + pending.mController->CreationFailed(); + } else { + uncancelled.AppendElement(pending); + } + } + + std::swap(self->mPendings, uncancelled); + }); + + bgEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + } + }; + + LOG(("LaunchNewContentProcess: remoteType=%s", aData.remoteType().get())); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [callback = std::move(processLaunchCallback), + workerRemoteType = aData.remoteType()]() mutable { + auto remoteType = + workerRemoteType.IsEmpty() ? DEFAULT_REMOTE_TYPE : workerRemoteType; + + RefPtr<LaunchPromiseType> onFinished; + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + // Request a process making sure to specify aPreferUsed=true. For a + // given remoteType there's a pool size limit. If we pass aPreferUsed + // here, then if there's any process in the pool already, we will use + // that. If we pass false (which is the default if omitted), then + // this call will spawn a new process if the pool isn't at its limit + // yet. + // + // (Our intent is never to grow the pool size here. Our logic gets + // here because our current logic on PBackground is only aware of + // RemoteWorkerServiceParent actors that have registered themselves, + // which is fundamentally unaware of processes that will match in the + // future when they register. So we absolutely are fine with and want + // any existing processes.) + onFinished = ContentParent::GetNewOrUsedBrowserProcessAsync( + /* aRemoteType = */ remoteType, + /* aGroup */ nullptr, + hal::ProcessPriority::PROCESS_PRIORITY_FOREGROUND, + /* aPreferUsed */ true); + } else { + // We can find this event still in flight after having been asked to + // shutdown. Let's fake a failure to ensure our callback is called + // such that we clean up everything properly. + onFinished = LaunchPromiseType::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + onFinished->Then(GetCurrentSerialEventTarget(), __func__, + [callback = std::move(callback), + remoteType](const CallbackParamType& aValue) mutable { + callback(aValue, remoteType); + }); + }); + + SchedulerGroup::Dispatch(r.forget()); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerManager.h b/dom/workers/remoteworkers/RemoteWorkerManager.h new file mode 100644 index 0000000000..5ff11ee6e6 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerManager.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerManager_h +#define mozilla_dom_RemoteWorkerManager_h + +#include "base/process.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/WorkerPrivate.h" // WorkerKind enum +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class RemoteWorkerController; +class RemoteWorkerServiceParent; + +/** + * PBackground instance that keeps tracks of RemoteWorkerServiceParent actors + * (1 per process, including the main process) and pending + * RemoteWorkerController requests to spawn remote workers if the spawn request + * can't be immediately fulfilled. Decides which RemoteWorkerServerParent to use + * internally via SelectTargetActor in order to select a BackgroundParent + * manager on which to create a RemoteWorkerParent. + */ +class RemoteWorkerManager final { + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerManager) + + static already_AddRefed<RemoteWorkerManager> GetOrCreate(); + + void RegisterActor(RemoteWorkerServiceParent* aActor); + + void UnregisterActor(RemoteWorkerServiceParent* aActor); + + void Launch(RemoteWorkerController* aController, + const RemoteWorkerData& aData, base::ProcessId aProcessId); + + static bool MatchRemoteType(const nsACString& processRemoteType, + const nsACString& workerRemoteType); + + /** + * Get the child process RemoteType where a RemoteWorker should be + * launched. + */ + static Result<nsCString, nsresult> GetRemoteType( + const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind); + + /** + * Verify if a remote worker should be allowed to run in the current + * child process remoteType. + */ + static bool IsRemoteTypeAllowed(const RemoteWorkerData& aData); + + static bool HasExtensionPrincipal(const RemoteWorkerData& aData); + + private: + RemoteWorkerManager(); + ~RemoteWorkerManager(); + + RemoteWorkerServiceParent* SelectTargetActor(const RemoteWorkerData& aData, + base::ProcessId aProcessId); + + RemoteWorkerServiceParent* SelectTargetActorInternal( + const RemoteWorkerData& aData, base::ProcessId aProcessId) const; + + void LaunchInternal(RemoteWorkerController* aController, + RemoteWorkerServiceParent* aTargetActor, + const RemoteWorkerData& aData, + bool aRemoteWorkerAlreadyRegistered = false); + + void LaunchNewContentProcess(const RemoteWorkerData& aData); + + void AsyncCreationFailed(RemoteWorkerController* aController); + + // Iterate through all RemoteWorkerServiceParent actors with the given + // remoteType, starting from the actor related to a child process with pid + // aProcessId if needed and available or from a random index otherwise (as if + // iterating through a circular array). + // + // aCallback should be a invokable object with a function signature of + // bool (RemoteWorkerServiceParent*, RefPtr<ContentParent>&&) + // + // aCallback is called with the actor and corresponding ContentParent, should + // return false to abort iteration before all actors have been traversed (e.g. + // if the desired actor is found), and must not mutate mChildActors (which + // shouldn't be an issue because this function is const). aCallback also + // doesn't need to worry about proxy-releasing the ContentParent if it isn't + // moved out of the parameter. + template <typename Callback> + void ForEachActor(Callback&& aCallback, const nsACString& aRemoteType, + Maybe<base::ProcessId> aProcessId = Nothing()) const; + + // The list of existing RemoteWorkerServiceParent actors for child processes. + // Raw pointers because RemoteWorkerServiceParent actors unregister themselves + // when destroyed. + // XXX For Fission, where we could have a lot of child actors, should we maybe + // instead keep either a hash table (PID->actor) or perhaps store the actors + // in order, sorted by PID, to avoid linear lookup times? + nsTArray<RemoteWorkerServiceParent*> mChildActors; + RemoteWorkerServiceParent* mParentActor; + + struct Pending { + RefPtr<RemoteWorkerController> mController; + RemoteWorkerData mData; + }; + + nsTArray<Pending> mPendings; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerManager_h diff --git a/dom/workers/remoteworkers/RemoteWorkerParent.cpp b/dom/workers/remoteworkers/RemoteWorkerParent.cpp new file mode 100644 index 0000000000..1a9613b799 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerParent.cpp @@ -0,0 +1,200 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerParent.h" +#include "RemoteWorkerController.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/PFetchEventOpProxyParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Unused.h" +#include "nsProxyRelease.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +class UnregisterActorRunnable final : public Runnable { + public: + explicit UnregisterActorRunnable( + already_AddRefed<ThreadsafeContentParentHandle> aParent) + : Runnable("UnregisterActorRunnable"), mContentHandle(aParent) { + AssertIsOnBackgroundThread(); + } + + NS_IMETHOD + Run() override { + AssertIsOnMainThread(); + if (RefPtr<ContentParent> contentParent = + mContentHandle->GetContentParent()) { + contentParent->UnregisterRemoveWorkerActor(); + } + + return NS_OK; + } + + private: + RefPtr<ThreadsafeContentParentHandle> mContentHandle; +}; + +} // namespace + +RemoteWorkerParent::RemoteWorkerParent() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); +} + +RemoteWorkerParent::~RemoteWorkerParent() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); +} + +void RemoteWorkerParent::Initialize(bool aAlreadyRegistered) { + RefPtr<ThreadsafeContentParentHandle> parent = + BackgroundParent::GetContentParentHandle(Manager()); + + // Parent is null if the child actor runs on the parent process. + if (parent) { + if (!aAlreadyRegistered) { + parent->RegisterRemoteWorkerActor(); + } + + NS_ReleaseOnMainThread("RemoteWorkerParent::Initialize ContentParent", + parent.forget()); + } +} + +already_AddRefed<PFetchEventOpProxyParent> +RemoteWorkerParent::AllocPFetchEventOpProxyParent( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) { + MOZ_CRASH("PFetchEventOpProxyParent actors must be manually constructed!"); + return nullptr; +} + +void RemoteWorkerParent::ActorDestroy(IProtocol::ActorDestroyReason) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + RefPtr<ThreadsafeContentParentHandle> parent = + BackgroundParent::GetContentParentHandle(Manager()); + + // Parent is null if the child actor runs on the parent process. + if (parent) { + RefPtr<UnregisterActorRunnable> r = + new UnregisterActorRunnable(parent.forget()); + SchedulerGroup::Dispatch(r.forget()); + } + + if (mController) { + mController->NoteDeadWorkerActor(); + mController = nullptr; + } +} + +IPCResult RemoteWorkerParent::RecvCreated(const bool& aStatus) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!mController) { + return IPC_OK(); + } + + if (aStatus) { + mController->CreationSucceeded(); + } else { + mController->CreationFailed(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerParent::RecvError(const ErrorValue& aValue) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->ErrorPropagation(aValue); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerParent::RecvNotifyLock(const bool& aCreated) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->NotifyLock(aCreated); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerParent::RecvNotifyWebTransport(const bool& aCreated) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->NotifyWebTransport(aCreated); + } + + return IPC_OK(); +} + +void RemoteWorkerParent::MaybeSendDelete() { + if (mDeleteSent) { + return; + } + + // For some reason, if the following two lines are swapped, ASan says there's + // a UAF... + mDeleteSent = true; + Unused << Send__delete__(this); +} + +IPCResult RemoteWorkerParent::RecvClose() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->WorkerTerminated(); + } + + MaybeSendDelete(); + + return IPC_OK(); +} + +void RemoteWorkerParent::SetController(RemoteWorkerController* aController) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + mController = aController; +} + +IPCResult RemoteWorkerParent::RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->SetServiceWorkerSkipWaitingFlag()->Then( + GetCurrentSerialEventTarget(), __func__, + [resolve = aResolve](bool /* unused */) { resolve(true); }, + [resolve = aResolve](nsresult /* unused */) { resolve(false); }); + } else { + aResolve(false); + } + + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerParent.h b/dom/workers/remoteworkers/RemoteWorkerParent.h new file mode 100644 index 0000000000..811206eb91 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerParent.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerParent_h +#define mozilla_dom_RemoteWorkerParent_h + +#include "mozilla/dom/PRemoteWorkerParent.h" + +namespace mozilla::dom { + +class RemoteWorkerController; + +/** + * PBackground-managed parent actor that is mutually associated with a single + * RemoteWorkerController. Relays error/close events to the controller and in + * turns is told life-cycle events. + */ +class RemoteWorkerParent final : public PRemoteWorkerParent { + friend class PRemoteWorkerParent; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerParent, override); + + RemoteWorkerParent(); + + void Initialize(bool aAlreadyRegistered = false); + + void SetController(RemoteWorkerController* aController); + + void MaybeSendDelete(); + + private: + ~RemoteWorkerParent(); + + already_AddRefed<PFetchEventOpProxyParent> AllocPFetchEventOpProxyParent( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs); + + void ActorDestroy(mozilla::ipc::IProtocol::ActorDestroyReason) override; + + mozilla::ipc::IPCResult RecvError(const ErrorValue& aValue); + + mozilla::ipc::IPCResult RecvNotifyLock(const bool& aCreated); + + mozilla::ipc::IPCResult RecvNotifyWebTransport(const bool& aCreated); + + mozilla::ipc::IPCResult RecvClose(); + + mozilla::ipc::IPCResult RecvCreated(const bool& aStatus); + + mozilla::ipc::IPCResult RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve); + + bool mDeleteSent = false; + RefPtr<RemoteWorkerController> mController; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerParent_h diff --git a/dom/workers/remoteworkers/RemoteWorkerService.cpp b/dom/workers/remoteworkers/RemoteWorkerService.cpp new file mode 100644 index 0000000000..5a33160c04 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerService.cpp @@ -0,0 +1,345 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerService.h" + +#include "mozilla/dom/PRemoteWorkerParent.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "nsIObserverService.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "nsXPCOMPrivate.h" +#include "RemoteWorkerController.h" +#include "RemoteWorkerServiceChild.h" +#include "RemoteWorkerServiceParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +StaticMutex sRemoteWorkerServiceMutex; +StaticRefPtr<RemoteWorkerService> sRemoteWorkerService; + +} // namespace + +/** + * Block shutdown until the RemoteWorkers have shutdown so that we do not try + * and shutdown the RemoteWorkerService "Worker Launcher" thread until they have + * cleanly shutdown. + * + * Note that this shutdown blocker is not used to initiate shutdown of any of + * the workers directly; their shutdown is initiated from PBackground in the + * parent process. The shutdown blocker just exists to avoid races around + * shutting down the worker launcher thread after all of the workers have + * shutdown and torn down their actors. + * + * Currently, it should be the case that the ContentParent should want to keep + * the content processes alive until the RemoteWorkers have all reported their + * shutdown over IPC (on the "Worker Launcher" thread). So for an orderly + * content process shutdown that is waiting for there to no longer be a reason + * to keep the content process alive, this blocker should only hang around for + * a brief period of time, helping smooth out lifecycle edge cases. + * + * In the event the content process is trying to shutdown while the + * RemoteWorkers think they should still be alive, it's possible that this + * blocker could expose the relevant logic error in the parent process if no + * attempt is made to shutdown the RemoteWorker. + * + * ## Major Implementation Note: This is not actually an nsIAsyncShutdownClient + * + * Until https://bugzilla.mozilla.org/show_bug.cgi?id=1760855 provides us with a + * non-JS implementation of nsIAsyncShutdownService, this implementation + * actually uses event loop spinning. The patch on + * https://bugzilla.mozilla.org/show_bug.cgi?id=1775784 that changed us to use + * this hack can be reverted when the time is right. + * + * Event loop spinning is handled by `RemoteWorkerService::Observe` and it calls + * our exposed `ShouldBlockShutdown()` to know when to stop spinning. + */ +class RemoteWorkerServiceShutdownBlocker final { + ~RemoteWorkerServiceShutdownBlocker() = default; + + public: + explicit RemoteWorkerServiceShutdownBlocker(RemoteWorkerService* aService) + : mService(aService), mBlockShutdown(true) {} + + void RemoteWorkersAllGoneAllowShutdown() { + mService->FinishShutdown(); + mService = nullptr; + + mBlockShutdown = false; + } + + bool ShouldBlockShutdown() { return mBlockShutdown; } + + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerServiceShutdownBlocker); + + RefPtr<RemoteWorkerService> mService; + bool mBlockShutdown; +}; + +RemoteWorkerServiceKeepAlive::RemoteWorkerServiceKeepAlive( + RemoteWorkerServiceShutdownBlocker* aBlocker) + : mBlocker(aBlocker) { + MOZ_ASSERT(NS_IsMainThread()); +} + +RemoteWorkerServiceKeepAlive::~RemoteWorkerServiceKeepAlive() { + // Dispatch a runnable to the main thread to tell the Shutdown Blocker to + // remove itself and notify the RemoteWorkerService it can finish its + // shutdown. We dispatch this to the main thread even if we are already on + // the main thread. + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [blocker = std::move(mBlocker)] { + blocker->RemoteWorkersAllGoneAllowShutdown(); + }); + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); +} + +/* static */ +void RemoteWorkerService::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + MOZ_ASSERT(!sRemoteWorkerService); + + RefPtr<RemoteWorkerService> service = new RemoteWorkerService(); + + // ## Content Process Initialization Case + // + // We are being told to initialize now that we know what our remote type is. + // Now is a fine time to call InitializeOnMainThread. + if (!XRE_IsParentProcess()) { + nsresult rv = service->InitializeOnMainThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + sRemoteWorkerService = service; + return; + } + // ## Parent Process Initialization Case + // + // Otherwise we are in the parent process and were invoked by + // nsLayoutStatics::Initialize. We wait until profile-after-change to kick + // off the Worker Launcher thread and have it connect to PBackground. This is + // an appropriate time for remote worker APIs to come online, especially + // because the PRemoteWorkerService mechanism needs processes to eagerly + // register themselves with PBackground since the design explicitly intends to + // avoid blocking on the main threads. (Disclaimer: Currently, things block + // on the main thread.) + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return; + } + + nsresult rv = obs->AddObserver(service, "profile-after-change", false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + sRemoteWorkerService = service; +} + +/* static */ +nsIThread* RemoteWorkerService::Thread() { + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + MOZ_ASSERT(sRemoteWorkerService); + MOZ_ASSERT(sRemoteWorkerService->mThread); + return sRemoteWorkerService->mThread; +} + +/* static */ +already_AddRefed<RemoteWorkerServiceKeepAlive> +RemoteWorkerService::MaybeGetKeepAlive() { + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + // In normal operation no one should be calling this without a service + // existing, so assert, but we'll also handle this being null as it is a + // plausible shutdown race. + MOZ_ASSERT(sRemoteWorkerService); + if (!sRemoteWorkerService) { + return nullptr; + } + + // Note that this value can be null, but this all handles that. + auto lockedKeepAlive = sRemoteWorkerService->mKeepAlive.Lock(); + RefPtr<RemoteWorkerServiceKeepAlive> extraRef = *lockedKeepAlive; + return extraRef.forget(); +} + +nsresult RemoteWorkerService::InitializeOnMainThread() { + // I would like to call this thread "DOM Remote Worker Launcher", but the max + // length is 16 chars. + nsresult rv = NS_NewNamedThread("Worker Launcher", getter_AddRefs(mThread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mShutdownBlocker = new RemoteWorkerServiceShutdownBlocker(this); + + { + RefPtr<RemoteWorkerServiceKeepAlive> keepAlive = + new RemoteWorkerServiceKeepAlive(mShutdownBlocker); + + auto lockedKeepAlive = mKeepAlive.Lock(); + *lockedKeepAlive = std::move(keepAlive); + } + + RefPtr<RemoteWorkerService> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "InitializeThread", [self]() { self->InitializeOnTargetThread(); }); + + rv = mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +RemoteWorkerService::RemoteWorkerService() + : mKeepAlive(nullptr, "RemoteWorkerService::mKeepAlive") { + MOZ_ASSERT(NS_IsMainThread()); +} + +RemoteWorkerService::~RemoteWorkerService() = default; + +void RemoteWorkerService::InitializeOnTargetThread() { + MOZ_ASSERT(mThread); + MOZ_ASSERT(mThread->IsOnCurrentThread()); + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return; + } + + RefPtr<RemoteWorkerServiceChild> serviceActor = + MakeAndAddRef<RemoteWorkerServiceChild>(); + if (NS_WARN_IF(!backgroundActor->SendPRemoteWorkerServiceConstructor( + serviceActor))) { + return; + } + + // Now we are ready! + mActor = serviceActor; +} + +void RemoteWorkerService::CloseActorOnTargetThread() { + MOZ_ASSERT(mThread); + MOZ_ASSERT(mThread->IsOnCurrentThread()); + + // If mActor is nullptr it means that initialization failed. + if (mActor) { + // Here we need to shutdown the IPC protocol. + mActor->Send__delete__(mActor); + mActor = nullptr; + } +} + +NS_IMETHODIMP +RemoteWorkerService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + MOZ_ASSERT(mThread); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + // Note that nsObserverList::NotifyObservers will hold a strong reference to + // our instance throughout the entire duration of this call, so it is not + // necessary for us to hold a kungFuDeathGrip here. + + // Drop our keep-alive. This could immediately result in our blocker saying + // it's okay for us to shutdown. SpinEventLoopUntil checks the predicate + // before spinning, so in the ideal case we will not spin the loop at all. + BeginShutdown(); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "RemoteWorkerService::Observe"_ns, + [&]() { return !mShutdownBlocker->ShouldBlockShutdown(); })); + + mShutdownBlocker = nullptr; + + return NS_OK; + } + + MOZ_ASSERT(!strcmp(aTopic, "profile-after-change")); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "profile-after-change"); + } + + return InitializeOnMainThread(); +} + +void RemoteWorkerService::BeginShutdown() { + // Drop our keepalive reference which may allow near-immediate removal of the + // blocker. + auto lockedKeepAlive = mKeepAlive.Lock(); + *lockedKeepAlive = nullptr; +} + +void RemoteWorkerService::FinishShutdown() { + // Clear the singleton before spinning the event loop when shutting down the + // thread so that MaybeGetKeepAlive() can assert if there are any late calls + // and to better reflect the actual state. + // + // Our caller, the RemoteWorkerServiceShutdownBlocker, will continue to hold a + // strong reference to us until we return from this call, so there are no + // lifecycle implications to dropping this reference. + { + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + sRemoteWorkerService = nullptr; + } + + RefPtr<RemoteWorkerService> self = this; + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction("RemoteWorkerService::CloseActorOnTargetThread", + [self]() { self->CloseActorOnTargetThread(); }); + + mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + + // We've posted a shutdown message; now shutdown the thread. This will spin + // a nested event loop waiting for the thread to process all pending events + // (including the just dispatched CloseActorOnTargetThread which will close + // the actor), ensuring to block main thread shutdown long enough to avoid + // races. + mThread->Shutdown(); + mThread = nullptr; +} + +NS_IMPL_ISUPPORTS(RemoteWorkerService, nsIObserver) + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerService.h b/dom/workers/remoteworkers/RemoteWorkerService.h new file mode 100644 index 0000000000..9e05e3958b --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerService.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerService_h +#define mozilla_dom_RemoteWorkerService_h + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/DataMutex.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsISupportsImpl.h" + +class nsIThread; + +namespace mozilla::dom { + +class RemoteWorkerService; +class RemoteWorkerServiceChild; +class RemoteWorkerServiceShutdownBlocker; + +/** + * Refcounted lifecycle helper; when its refcount goes to zero its destructor + * will call RemoteWorkerService::Shutdown() which will remove the shutdown + * blocker and shutdown the "Worker Launcher" thread. + * + * The RemoteWorkerService itself will hold a reference to this singleton which + * it will use to hand out additional refcounts to RemoteWorkerChild instances. + * When the shutdown blocker is notified that it's time to shutdown, the + * RemoteWorkerService's reference will be dropped. + */ +class RemoteWorkerServiceKeepAlive { + public: + explicit RemoteWorkerServiceKeepAlive( + RemoteWorkerServiceShutdownBlocker* aBlocker); + + private: + ~RemoteWorkerServiceKeepAlive(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteWorkerServiceKeepAlive); + + RefPtr<RemoteWorkerServiceShutdownBlocker> mBlocker; +}; + +/** + * Every process has a RemoteWorkerService which does the actual spawning of + * RemoteWorkerChild instances. The RemoteWorkerService creates a "Worker + * Launcher" thread at initialization on which it creates a + * RemoteWorkerServiceChild to service spawn requests. The thread is exposed as + * RemoteWorkerService::Thread(). A new/distinct thread is used because we + * (eventually) don't want to deal with main-thread contention, content + * processes have no equivalent of a PBackground thread, and actors are bound to + * specific threads. + * + * (Disclaimer: currently most RemoteWorkerOps need to happen on the main thread + * because the main-thread ends up as the owner of the worker and all + * manipulation of the worker must happen from the owning thread.) + */ +class RemoteWorkerService final : public nsIObserver { + friend class RemoteWorkerServiceShutdownBlocker; + friend class RemoteWorkerServiceKeepAlive; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + // To be called when a process is initialized on main-thread. + static void Initialize(); + + static nsIThread* Thread(); + + // Called by RemoteWorkerChild instances on the "Worker Launcher" thread at + // their creation to assist in tracking when it's safe to shutdown the + // RemoteWorkerService and "Worker Launcher" thread. This method will return + // a null pointer if the RemoteWorkerService has already begun shutdown. + // + // This is somewhat awkwardly a static method because the RemoteWorkerChild + // instances are not managed by RemoteWorkerServiceChild, but instead are + // managed by PBackground(Child). So we either need to find the + // RemoteWorkerService via the hidden singleton or by having the + // RemoteWorkerChild use PBackgroundChild::ManagedPRemoteWorkerServiceChild() + // to locate the instance. We are choosing to use the singleton because we + // already need to acquire a mutex in the call regardless and the upcoming + // refactorings may want to start using new toplevel protocols and this will + // avoid requiring a change when that happens. + static already_AddRefed<RemoteWorkerServiceKeepAlive> MaybeGetKeepAlive(); + + private: + RemoteWorkerService(); + ~RemoteWorkerService(); + + nsresult InitializeOnMainThread(); + + void InitializeOnTargetThread(); + + void CloseActorOnTargetThread(); + + // Called by RemoteWorkerServiceShutdownBlocker when it's time to drop the + // RemoteWorkerServiceKeepAlive reference. + void BeginShutdown(); + + // Called by RemoteWorkerServiceShutdownBlocker when the blocker has been + // removed and it's safe to shutdown the "Worker Launcher" thread. + void FinishShutdown(); + + nsCOMPtr<nsIThread> mThread; + RefPtr<RemoteWorkerServiceChild> mActor; + // The keep-alive is set and cleared on the main thread but we will hand out + // additional references to it from the "Worker Launcher" thread, so it's + // appropriate to use a mutex. (Alternately we could have used a ThreadBound + // and set and cleared on the "Worker Launcher" thread, but that would + // involve more moving parts and could have complicated edge cases.) + DataMutex<RefPtr<RemoteWorkerServiceKeepAlive>> mKeepAlive; + // In order to poll the blocker to know when we can stop spinning the event + // loop at shutdown, we retain a reference to the blocker. + RefPtr<RemoteWorkerServiceShutdownBlocker> mShutdownBlocker; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerService_h diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceChild.cpp b/dom/workers/remoteworkers/RemoteWorkerServiceChild.cpp new file mode 100644 index 0000000000..100908064e --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceChild.cpp @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerServiceChild.h" +#include "RemoteWorkerController.h" + +namespace mozilla::dom { + +RemoteWorkerServiceChild::RemoteWorkerServiceChild() = default; + +RemoteWorkerServiceChild::~RemoteWorkerServiceChild() = default; + +} // namespace mozilla::dom diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceChild.h b/dom/workers/remoteworkers/RemoteWorkerServiceChild.h new file mode 100644 index 0000000000..41ae879b29 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceChild.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerServiceChild_h +#define mozilla_dom_RemoteWorkerServiceChild_h + +#include "mozilla/dom/PRemoteWorkerServiceChild.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +class RemoteWorkerController; +class RemoteWorkerData; + +/** + * "Worker Launcher"-thread child actor created by the RemoteWorkerService to + * register itself with the PBackground RemoteWorkerManager in the parent. + */ +class RemoteWorkerServiceChild final : public PRemoteWorkerServiceChild { + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerServiceChild, final) + + RemoteWorkerServiceChild(); + + private: + ~RemoteWorkerServiceChild(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerServiceChild_h diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceParent.cpp b/dom/workers/remoteworkers/RemoteWorkerServiceParent.cpp new file mode 100644 index 0000000000..c7fd5ea6c8 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceParent.cpp @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerServiceParent.h" +#include "RemoteWorkerManager.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +RemoteWorkerServiceParent::RemoteWorkerServiceParent() + : mManager(RemoteWorkerManager::GetOrCreate()) {} + +RemoteWorkerServiceParent::~RemoteWorkerServiceParent() = default; + +void RemoteWorkerServiceParent::Initialize(const nsACString& aRemoteType) { + AssertIsOnBackgroundThread(); + mRemoteType = aRemoteType; + mManager->RegisterActor(this); +} + +void RemoteWorkerServiceParent::ActorDestroy(IProtocol::ActorDestroyReason) { + AssertIsOnBackgroundThread(); + mManager->UnregisterActor(this); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceParent.h b/dom/workers/remoteworkers/RemoteWorkerServiceParent.h new file mode 100644 index 0000000000..31ed29a78c --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceParent.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerServiceParent_h +#define mozilla_dom_RemoteWorkerServiceParent_h + +#include "mozilla/dom/PRemoteWorkerServiceParent.h" +#include "mozilla/dom/RemoteType.h" + +namespace mozilla::dom { + +class RemoteWorkerManager; + +/** + * PBackground parent actor that registers with the PBackground + * RemoteWorkerManager and used to relay spawn requests. + */ +class RemoteWorkerServiceParent final : public PRemoteWorkerServiceParent { + public: + RemoteWorkerServiceParent(); + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerServiceParent, override); + + void ActorDestroy(mozilla::ipc::IProtocol::ActorDestroyReason) override; + + void Initialize(const nsACString& aRemoteType); + + nsCString GetRemoteType() const { return mRemoteType; } + + private: + ~RemoteWorkerServiceParent(); + + RefPtr<RemoteWorkerManager> mManager; + nsCString mRemoteType = NOT_REMOTE_TYPE; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerServiceParent_h diff --git a/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh new file mode 100644 index 0000000000..8894450b72 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include ClientIPCTypes; +include IPCServiceWorkerDescriptor; +include IPCServiceWorkerRegistrationDescriptor; +include PBackgroundSharedTypes; +include URIParams; +include DOMTypes; +include NeckoChannelParams; +include ProtocolTypes; + +include "mozilla/dom/ClientIPCUtils.h"; +include "mozilla/dom/ReferrerInfoUtils.h"; +include "mozilla/dom/WorkerIPCUtils.h"; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; +using mozilla::dom::RequestCredentials from "mozilla/dom/RequestBinding.h"; +using mozilla::StorageAccess from "mozilla/StorageAccess.h"; +using mozilla::OriginTrials from "mozilla/OriginTrialsIPCUtils.h"; +using mozilla::dom::WorkerType from "mozilla/dom/WorkerBinding.h"; + +namespace mozilla { +namespace dom { + +struct ServiceWorkerData { + IPCServiceWorkerDescriptor descriptor; + IPCServiceWorkerRegistrationDescriptor registrationDescriptor; + nsString cacheName; + uint32_t loadFlags; + nsString id; +}; + +union OptionalServiceWorkerData { + void_t; + ServiceWorkerData; +}; + +struct RemoteWorkerData +{ + // This should only be used for devtools. + nsString originalScriptURL; + + // It is important to pass these as URIParams instead of strings for blob + // URLs: they carry an additional bit of state with them (mIsRevoked) that + // gives us a chance to use them, even after they've been revoked. Because + // we're asynchronously calling into the parent process before potentially + // loading the worker, it is important to keep this state. Note that this + // isn't a panacea: once the URL has been revoked, it'll give the worker 5 + // seconds to actually load it; so it's possible to still fail to load the + // blob URL if it takes too long to do the round trip. + URIParams baseScriptURL; + URIParams resolvedScriptURL; + + nsString name; + WorkerType type; + RequestCredentials credentials; + + PrincipalInfo loadingPrincipalInfo; + PrincipalInfo principalInfo; + PrincipalInfo partitionedPrincipalInfo; + + bool useRegularPrincipal; + bool usingStorageAccess; + + CookieJarSettingsArgs cookieJarSettings; + + nsCString domain; + + bool isSecureContext; + + IPCClientInfo? clientInfo; + + nullable nsIReferrerInfo referrerInfo; + + StorageAccess storageAccess; + + bool isThirdPartyContextToTopWindow; + + bool shouldResistFingerprinting; + + uint64_t? overriddenFingerprintingSettings; + + OriginTrials originTrials; + + OptionalServiceWorkerData serviceWorkerData; + + nsID agentClusterId; + + // Child process remote type where the worker should only run on. + nsCString remoteType; +}; + +// ErrorData/ErrorDataNote correspond to WorkerErrorReport/WorkerErrorNote +// which in turn correspond to JSErrorReport/JSErrorNotes which allows JS to +// report complicated errors such as redeclarations that involve multiple +// distinct lines. For more generic error-propagation IPC structures, see bug +// 1357463 on making ErrorResult usable over IPC. + +struct ErrorDataNote { + uint32_t lineNumber; + uint32_t columnNumber; + nsString message; + nsString filename; +}; + +struct ErrorData { + bool isWarning; + uint32_t lineNumber; + uint32_t columnNumber; + nsString message; + nsString filename; + nsString line; + ErrorDataNote[] notes; +}; + +struct CSPViolation { + nsString json; +}; + +union ErrorValue { + nsresult; + ErrorData; + CSPViolation; + void_t; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/moz.build b/dom/workers/remoteworkers/moz.build new file mode 100644 index 0000000000..9983b7dd10 --- /dev/null +++ b/dom/workers/remoteworkers/moz.build @@ -0,0 +1,45 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "RemoteWorkerChild.h", + "RemoteWorkerController.h", + "RemoteWorkerControllerChild.h", + "RemoteWorkerControllerParent.h", + "RemoteWorkerManager.h", + "RemoteWorkerParent.h", + "RemoteWorkerService.h", + "RemoteWorkerServiceChild.h", + "RemoteWorkerServiceParent.h", +] + +UNIFIED_SOURCES += [ + "RemoteWorkerChild.cpp", + "RemoteWorkerController.cpp", + "RemoteWorkerControllerChild.cpp", + "RemoteWorkerControllerParent.cpp", + "RemoteWorkerManager.cpp", + "RemoteWorkerParent.cpp", + "RemoteWorkerService.cpp", + "RemoteWorkerServiceChild.cpp", + "RemoteWorkerServiceParent.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/serviceworkers", + "/xpcom/build", +] + +IPDL_SOURCES += [ + "PRemoteWorker.ipdl", + "PRemoteWorkerController.ipdl", + "PRemoteWorkerService.ipdl", + "RemoteWorkerTypes.ipdlh", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/workers/sharedworkers/PSharedWorker.ipdl b/dom/workers/sharedworkers/PSharedWorker.ipdl new file mode 100644 index 0000000000..2406a731da --- /dev/null +++ b/dom/workers/sharedworkers/PSharedWorker.ipdl @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include RemoteWorkerTypes; + +namespace mozilla { +namespace dom { + +/** + * Protocol for SharedWorker bindings to communicate with per-worker + * SharedWorkerManager instances in the parent via SharedWorkerChild / + * SharedWorkerParent and SharedWorkerService getting/creating the + * SharedWorkerManager if it doesn't already exist. Main-thread to PBackground. + */ +[ManualDealloc] +protocol PSharedWorker +{ + manager PBackground; + +parent: + async Close(); + async Suspend(); + async Resume(); + async Freeze(); + async Thaw(); + +child: + async Error(ErrorValue value); + async NotifyLock(bool aCreated); + async NotifyWebTransport(bool aCreated); + async Terminate(); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/sharedworkers/SharedWorker.cpp b/dom/workers/sharedworkers/SharedWorker.cpp new file mode 100644 index 0000000000..d3bb3bfcaa --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorker.cpp @@ -0,0 +1,445 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SharedWorker.h" + +#include "mozilla/AntiTrackingUtils.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/MessageChannel.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/PMessagePort.h" +#include "mozilla/dom/RemoteWorkerManager.h" // RemoteWorkerManager::GetRemoteType +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/SharedWorkerBinding.h" +#include "mozilla/dom/SharedWorkerChild.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/WorkerLoadInfo.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/StorageAccess.h" +#include "nsGlobalWindowInner.h" +#include "nsPIDOMWindow.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +SharedWorker::SharedWorker(nsPIDOMWindowInner* aWindow, + SharedWorkerChild* aActor, MessagePort* aMessagePort) + : DOMEventTargetHelper(aWindow), + mWindow(aWindow), + mActor(aActor), + mMessagePort(aMessagePort), + mFrozen(false) { + AssertIsOnMainThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aMessagePort); +} + +SharedWorker::~SharedWorker() { + AssertIsOnMainThread(); + Close(); +} + +// static +already_AddRefed<SharedWorker> SharedWorker::Constructor( + const GlobalObject& aGlobal, const nsAString& aScriptURL, + const StringOrWorkerOptions& aOptions, ErrorResult& aRv) { + AssertIsOnMainThread(); + + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(window); + + // Our current idiom is that storage-related APIs specialize for the system + // principal themselves, which is consistent with StorageAllowedForwindow not + // specializing for the system principal. Without this specialization we + // would end up with ePrivateBrowsing for system principaled private browsing + // windows which is explicitly not what we want. System Principal code always + // should have access to storage. It may make sense to enhance + // StorageAllowedForWindow in the future to handle this after comprehensive + // auditing. + nsCOMPtr<nsIPrincipal> principal = aGlobal.GetSubjectPrincipal(); + StorageAccess storageAllowed; + if (principal && principal->IsSystemPrincipal()) { + storageAllowed = StorageAccess::eAllow; + } else { + storageAllowed = StorageAllowedForWindow(window); + } + + if (storageAllowed == StorageAccess::eDeny) { + aRv.ThrowSecurityError("StorageAccess denied."); + return nullptr; + } + + if (ShouldPartitionStorage(storageAllowed) && + !StoragePartitioningEnabled( + storageAllowed, window->GetExtantDoc()->CookieJarSettings())) { + aRv.ThrowSecurityError("StoragePartitioning not enabled."); + return nullptr; + } + + // Assert that the principal private browsing state matches the + // StorageAccess value. +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (storageAllowed == StorageAccess::ePrivateBrowsing) { + uint32_t privateBrowsingId = 0; + if (principal) { + MOZ_ALWAYS_SUCCEEDS(principal->GetPrivateBrowsingId(&privateBrowsingId)); + } + MOZ_DIAGNOSTIC_ASSERT(privateBrowsingId != 0); + } +#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED + + PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread(); + if (!actorChild || !actorChild->CanSend()) { + aRv.ThrowSecurityError("PBackground not available."); + return nullptr; + } + + nsAutoString name; + WorkerType workerType = WorkerType::Classic; + RequestCredentials credentials = RequestCredentials::Omit; + if (aOptions.IsString()) { + name = aOptions.GetAsString(); + } else { + MOZ_ASSERT(aOptions.IsWorkerOptions()); + name = aOptions.GetAsWorkerOptions().mName; + workerType = aOptions.GetAsWorkerOptions().mType; + credentials = aOptions.GetAsWorkerOptions().mCredentials; + } + + JSContext* cx = aGlobal.Context(); + + WorkerLoadInfo loadInfo; + aRv = WorkerPrivate::GetLoadInfo( + cx, window, nullptr, aScriptURL, workerType, credentials, false, + WorkerPrivate::OverrideLoadGroup, WorkerKindShared, &loadInfo); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + PrincipalInfo principalInfo; + aRv = PrincipalToPrincipalInfo(loadInfo.mPrincipal, &principalInfo); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + PrincipalInfo loadingPrincipalInfo; + aRv = PrincipalToPrincipalInfo(loadInfo.mLoadingPrincipal, + &loadingPrincipalInfo); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // Here, the PartitionedPrincipal is always equal to the SharedWorker's + // principal because the channel is not opened yet, and, because of this, it's + // not classified. We need to force the correct originAttributes. + // + // The sharedWorker's principal could be a null principal, e.g. loading a + // data url. In this case, we don't need to force the OAs for the partitioned + // principal because creating storage from a null principal will fail anyway. + // We should only do this for content principals. + // + // You can find more details in StoragePrincipalHelper.h + if (ShouldPartitionStorage(storageAllowed) && + BasePrincipal::Cast(loadInfo.mPrincipal)->IsContentPrincipal()) { + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(window); + if (!sop) { + aRv.ThrowSecurityError("ScriptObjectPrincipal not available."); + return nullptr; + } + + nsIPrincipal* windowPrincipal = sop->GetPrincipal(); + if (!windowPrincipal) { + aRv.ThrowSecurityError("WindowPrincipal not available."); + return nullptr; + } + + nsIPrincipal* windowPartitionedPrincipal = sop->PartitionedPrincipal(); + if (!windowPartitionedPrincipal) { + aRv.ThrowSecurityError("WindowPartitionedPrincipal not available."); + return nullptr; + } + + if (!windowPrincipal->Equals(windowPartitionedPrincipal)) { + loadInfo.mPartitionedPrincipal = + BasePrincipal::Cast(loadInfo.mPrincipal) + ->CloneForcingOriginAttributes( + BasePrincipal::Cast(windowPartitionedPrincipal) + ->OriginAttributesRef()); + } + } + + PrincipalInfo partitionedPrincipalInfo; + if (loadInfo.mPrincipal->Equals(loadInfo.mPartitionedPrincipal)) { + partitionedPrincipalInfo = principalInfo; + } else { + aRv = PrincipalToPrincipalInfo(loadInfo.mPartitionedPrincipal, + &partitionedPrincipalInfo); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + // We don't actually care about this MessageChannel, but we use it to 'steal' + // its 2 connected ports. + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(window); + RefPtr<MessageChannel> channel = MessageChannel::Constructor(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + UniqueMessagePortId portIdentifier; + channel->Port1()->CloneAndDisentangle(portIdentifier); + + URIParams resolvedScriptURL; + SerializeURI(loadInfo.mResolvedScriptURI, resolvedScriptURL); + + URIParams baseURL; + SerializeURI(loadInfo.mBaseURI, baseURL); + + // Register this component to PBackground. + bool isSecureContext = JS::GetIsSecureContext(js::GetContextRealm(cx)); + + Maybe<IPCClientInfo> ipcClientInfo; + Maybe<ClientInfo> clientInfo = window->GetClientInfo(); + if (clientInfo.isSome()) { + ipcClientInfo.emplace(clientInfo.value().ToIPC()); + } + + nsID agentClusterId = nsID::GenerateUUID(); + + net::CookieJarSettingsArgs cjsData; + MOZ_ASSERT(loadInfo.mCookieJarSettings); + net::CookieJarSettings::Cast(loadInfo.mCookieJarSettings)->Serialize(cjsData); + + auto remoteType = RemoteWorkerManager::GetRemoteType( + loadInfo.mPrincipal, WorkerKind::WorkerKindShared); + if (NS_WARN_IF(remoteType.isErr())) { + aRv.Throw(remoteType.unwrapErr()); + return nullptr; + } + + Maybe<uint64_t> overriddenFingerprintingSettingsArg; + if (loadInfo.mOverriddenFingerprintingSettings.isSome()) { + overriddenFingerprintingSettingsArg.emplace( + uint64_t(loadInfo.mOverriddenFingerprintingSettings.ref())); + } + + RemoteWorkerData remoteWorkerData( + nsString(aScriptURL), baseURL, resolvedScriptURL, name, workerType, + credentials, loadingPrincipalInfo, principalInfo, + partitionedPrincipalInfo, loadInfo.mUseRegularPrincipal, + loadInfo.mUsingStorageAccess, cjsData, loadInfo.mDomain, isSecureContext, + ipcClientInfo, loadInfo.mReferrerInfo, storageAllowed, + AntiTrackingUtils::IsThirdPartyWindow(window, nullptr), + loadInfo.mShouldResistFingerprinting, overriddenFingerprintingSettingsArg, + OriginTrials::FromWindow(nsGlobalWindowInner::Cast(window)), + void_t() /* OptionalServiceWorkerData */, agentClusterId, + remoteType.unwrap()); + + PSharedWorkerChild* pActor = actorChild->SendPSharedWorkerConstructor( + remoteWorkerData, loadInfo.mWindowID, portIdentifier.release()); + if (!pActor) { + MOZ_ASSERT_UNREACHABLE("We already checked PBackground above."); + aRv.ThrowSecurityError("PBackground not available."); + return nullptr; + } + + RefPtr<SharedWorkerChild> actor = static_cast<SharedWorkerChild*>(pActor); + + RefPtr<SharedWorker> sharedWorker = + new SharedWorker(window, actor, channel->Port2()); + + // Let's inform the window about this SharedWorker. + nsGlobalWindowInner::Cast(window)->StoreSharedWorker(sharedWorker); + actor->SetParent(sharedWorker); + + if (nsGlobalWindowInner::Cast(window)->IsSuspended()) { + sharedWorker->Suspend(); + } + + return sharedWorker.forget(); +} + +MessagePort* SharedWorker::Port() { + AssertIsOnMainThread(); + return mMessagePort; +} + +void SharedWorker::Freeze() { + AssertIsOnMainThread(); + MOZ_ASSERT(!IsFrozen()); + + if (mFrozen) { + return; + } + + mFrozen = true; + + if (mActor) { + mActor->SendFreeze(); + } +} + +void SharedWorker::Thaw() { + AssertIsOnMainThread(); + MOZ_ASSERT(IsFrozen()); + + if (!mFrozen) { + return; + } + + mFrozen = false; + + if (mActor) { + mActor->SendThaw(); + } + + if (!mFrozenEvents.IsEmpty()) { + nsTArray<RefPtr<Event>> events = std::move(mFrozenEvents); + + for (uint32_t index = 0; index < events.Length(); index++) { + RefPtr<Event>& event = events[index]; + MOZ_ASSERT(event); + + RefPtr<EventTarget> target = event->GetTarget(); + ErrorResult rv; + target->DispatchEvent(*event, rv); + if (rv.Failed()) { + NS_WARNING("Failed to dispatch event!"); + } + } + } +} + +void SharedWorker::QueueEvent(Event* aEvent) { + AssertIsOnMainThread(); + MOZ_ASSERT(aEvent); + MOZ_ASSERT(IsFrozen()); + + mFrozenEvents.AppendElement(aEvent); +} + +void SharedWorker::Close() { + AssertIsOnMainThread(); + + if (mWindow) { + nsGlobalWindowInner::Cast(mWindow)->ForgetSharedWorker(this); + mWindow = nullptr; + } + + if (mActor) { + mActor->SendClose(); + mActor->SetParent(nullptr); + mActor = nullptr; + } + + if (mMessagePort) { + mMessagePort->Close(); + } +} + +void SharedWorker::Suspend() { + if (mActor) { + mActor->SendSuspend(); + } +} + +void SharedWorker::Resume() { + if (mActor) { + mActor->SendResume(); + } +} + +void SharedWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, + ErrorResult& aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(mMessagePort); + + mMessagePort->PostMessage(aCx, aMessage, aTransferable, aRv); +} + +NS_IMPL_ADDREF_INHERITED(SharedWorker, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(SharedWorker, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SharedWorker) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_CLASS(SharedWorker) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SharedWorker, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrozenEvents) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(SharedWorker, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrozenEvents) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +JSObject* SharedWorker::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + AssertIsOnMainThread(); + + return SharedWorker_Binding::Wrap(aCx, this, aGivenProto); +} + +void SharedWorker::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + AssertIsOnMainThread(); + + if (IsFrozen()) { + RefPtr<Event> event = aVisitor.mDOMEvent; + if (!event) { + event = EventDispatcher::CreateEvent(aVisitor.mEvent->mOriginalTarget, + aVisitor.mPresContext, + aVisitor.mEvent, u""_ns); + } + + QueueEvent(event); + + aVisitor.mCanHandle = false; + aVisitor.SetParentTarget(nullptr, false); + return; + } + + DOMEventTargetHelper::GetEventTargetParent(aVisitor); +} + +void SharedWorker::DisconnectFromOwner() { + Close(); + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void SharedWorker::ErrorPropagation(nsresult aError) { + AssertIsOnMainThread(); + MOZ_ASSERT(mActor); + MOZ_ASSERT(NS_FAILED(aError)); + + RefPtr<AsyncEventDispatcher> errorEvent = + new AsyncEventDispatcher(this, u"error"_ns, CanBubble::eNo); + errorEvent->PostDOMEvent(); + + Close(); +} diff --git a/dom/workers/sharedworkers/SharedWorker.h b/dom/workers/sharedworkers/SharedWorker.h new file mode 100644 index 0000000000..be61de8889 --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorker.h @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_workers_sharedworker_h__ +#define mozilla_dom_workers_sharedworker_h__ + +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/DOMEventTargetHelper.h" + +#ifdef XP_WIN +# undef PostMessage +#endif + +class nsPIDOMWindowInner; + +namespace mozilla { +class EventChainPreVisitor; + +namespace dom { +class MessagePort; +class StringOrWorkerOptions; +class Event; + +class SharedWorkerChild; + +/** + * DOM binding. Holds a SharedWorkerChild. Must exist on the main thread because + * we only allow top-level windows to create SharedWorkers. + */ +class SharedWorker final : public DOMEventTargetHelper { + using ErrorResult = mozilla::ErrorResult; + using GlobalObject = mozilla::dom::GlobalObject; + + RefPtr<nsPIDOMWindowInner> mWindow; + RefPtr<SharedWorkerChild> mActor; + RefPtr<MessagePort> mMessagePort; + nsTArray<RefPtr<Event>> mFrozenEvents; + bool mFrozen; + + public: + static already_AddRefed<SharedWorker> Constructor( + const GlobalObject& aGlobal, const nsAString& aScriptURL, + const StringOrWorkerOptions& aOptions, ErrorResult& aRv); + + MessagePort* Port(); + + bool IsFrozen() const { return mFrozen; } + + void QueueEvent(Event* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SharedWorker, DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(error) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + void DisconnectFromOwner() override; + + void ErrorPropagation(nsresult aError); + + // Methods called from the window. + + void Close(); + + void Suspend(); + + void Resume(); + + void Freeze(); + + void Thaw(); + + private: + SharedWorker(nsPIDOMWindowInner* aWindow, SharedWorkerChild* aActor, + MessagePort* aMessagePort); + + // This class is reference-counted and will be destroyed from Release(). + ~SharedWorker(); + + // Only called by MessagePort. + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Sequence<JSObject*>& aTransferable, ErrorResult& aRv); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_sharedworker_h__ diff --git a/dom/workers/sharedworkers/SharedWorkerChild.cpp b/dom/workers/sharedworkers/SharedWorkerChild.cpp new file mode 100644 index 0000000000..21ae74681b --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerChild.cpp @@ -0,0 +1,181 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SharedWorkerChild.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/SecurityPolicyViolationEvent.h" +#include "mozilla/dom/SecurityPolicyViolationEventBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SharedWorker.h" +#include "mozilla/dom/WebTransport.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WorkerError.h" +#include "mozilla/dom/locks/LockManagerChild.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +SharedWorkerChild::SharedWorkerChild() : mParent(nullptr), mActive(true) {} + +SharedWorkerChild::~SharedWorkerChild() = default; + +void SharedWorkerChild::ActorDestroy(ActorDestroyReason aWhy) { + mActive = false; +} + +void SharedWorkerChild::SendClose() { + if (mActive) { + // This is the last message. + mActive = false; + PSharedWorkerChild::SendClose(); + } +} + +void SharedWorkerChild::SendSuspend() { + if (mActive) { + PSharedWorkerChild::SendSuspend(); + } +} + +void SharedWorkerChild::SendResume() { + if (mActive) { + PSharedWorkerChild::SendResume(); + } +} + +void SharedWorkerChild::SendFreeze() { + if (mActive) { + PSharedWorkerChild::SendFreeze(); + } +} + +void SharedWorkerChild::SendThaw() { + if (mActive) { + PSharedWorkerChild::SendThaw(); + } +} + +IPCResult SharedWorkerChild::RecvError(const ErrorValue& aValue) { + if (!mParent) { + return IPC_OK(); + } + + if (aValue.type() == ErrorValue::Tnsresult) { + mParent->ErrorPropagation(aValue.get_nsresult()); + return IPC_OK(); + } + + nsPIDOMWindowInner* window = mParent->GetOwner(); + uint64_t innerWindowId = window ? window->WindowID() : 0; + + if (aValue.type() == ErrorValue::TCSPViolation) { + SecurityPolicyViolationEventInit violationEventInit; + if (NS_WARN_IF( + !violationEventInit.Init(aValue.get_CSPViolation().json()))) { + return IPC_OK(); + } + + if (NS_WARN_IF(!window)) { + return IPC_OK(); + } + + RefPtr<EventTarget> eventTarget = window->GetExtantDoc(); + if (NS_WARN_IF(!eventTarget)) { + return IPC_OK(); + } + + RefPtr<Event> event = SecurityPolicyViolationEvent::Constructor( + eventTarget, u"securitypolicyviolation"_ns, violationEventInit); + event->SetTrusted(true); + + eventTarget->DispatchEvent(*event); + return IPC_OK(); + } + + if (aValue.type() == ErrorValue::TErrorData && + aValue.get_ErrorData().isWarning()) { + // Don't fire any events for warnings. Just log to console. + WorkerErrorReport::LogErrorToConsole(aValue.get_ErrorData(), innerWindowId); + return IPC_OK(); + } + + AutoJSAPI jsapi; + jsapi.Init(); + + RefPtr<Event> event; + if (aValue.type() == ErrorValue::TErrorData) { + const ErrorData& errorData = aValue.get_ErrorData(); + RootedDictionary<ErrorEventInit> errorInit(jsapi.cx()); + errorInit.mBubbles = false; + errorInit.mCancelable = true; + errorInit.mMessage = errorData.message(); + errorInit.mFilename = errorData.filename(); + errorInit.mLineno = errorData.lineNumber(); + errorInit.mColno = errorData.columnNumber(); + + event = ErrorEvent::Constructor(mParent, u"error"_ns, errorInit); + } else { + event = Event::Constructor(mParent, u"error"_ns, EventInit()); + } + event->SetTrusted(true); + + ErrorResult res; + bool defaultActionEnabled = + mParent->DispatchEvent(*event, CallerType::System, res); + if (res.Failed()) { + ThrowAndReport(window, res.StealNSResult()); + return IPC_OK(); + } + + if (aValue.type() != ErrorValue::TErrorData) { + MOZ_ASSERT(aValue.type() == ErrorValue::Tvoid_t); + return IPC_OK(); + } + + if (defaultActionEnabled) { + WorkerErrorReport::LogErrorToConsole(aValue.get_ErrorData(), innerWindowId); + } + + return IPC_OK(); +} + +IPCResult SharedWorkerChild::RecvNotifyLock(bool aCreated) { + if (!mParent) { + return IPC_OK(); + } + + locks::LockManagerChild::NotifyBFCacheOnMainThread(mParent->GetOwner(), + aCreated); + + return IPC_OK(); +} + +IPCResult SharedWorkerChild::RecvNotifyWebTransport(bool aCreated) { + if (!mParent) { + return IPC_OK(); + } + + WebTransport::NotifyBFCacheOnMainThread(mParent->GetOwner(), aCreated); + + return IPC_OK(); +} + +IPCResult SharedWorkerChild::RecvTerminate() { + if (mParent) { + mParent->Close(); + } + + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/sharedworkers/SharedWorkerChild.h b/dom/workers/sharedworkers/SharedWorkerChild.h new file mode 100644 index 0000000000..c899589320 --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerChild.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_dom_SharedWorkerChild_h +#define mozilla_dom_dom_SharedWorkerChild_h + +#include "mozilla/dom/PSharedWorkerChild.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +class SharedWorker; + +/** + * Held by SharedWorker bindings to remotely control sharedworker lifecycle and + * receive error and termination reports. + */ +class SharedWorkerChild final : public mozilla::dom::PSharedWorkerChild { + friend class PSharedWorkerChild; + + public: + NS_INLINE_DECL_REFCOUNTING(SharedWorkerChild) + + SharedWorkerChild(); + + void SetParent(SharedWorker* aSharedWorker) { mParent = aSharedWorker; } + + void SendClose(); + + void SendSuspend(); + + void SendResume(); + + void SendFreeze(); + + void SendThaw(); + + private: + ~SharedWorkerChild(); + + mozilla::ipc::IPCResult RecvError(const ErrorValue& aValue); + + mozilla::ipc::IPCResult RecvNotifyLock(bool aCreated); + + mozilla::ipc::IPCResult RecvNotifyWebTransport(bool aCreated); + + mozilla::ipc::IPCResult RecvTerminate(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + // Raw pointer because mParent is set to null when released. + SharedWorker* MOZ_NON_OWNING_REF mParent; + bool mActive; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_dom_SharedWorkerChild_h diff --git a/dom/workers/sharedworkers/SharedWorkerManager.cpp b/dom/workers/sharedworkers/SharedWorkerManager.cpp new file mode 100644 index 0000000000..37dbc700d9 --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerManager.cpp @@ -0,0 +1,348 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SharedWorkerManager.h" +#include "SharedWorkerParent.h" +#include "SharedWorkerService.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/PSharedWorker.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/dom/RemoteWorkerController.h" +#include "nsIConsoleReportCollector.h" +#include "nsIPrincipal.h" +#include "nsProxyRelease.h" + +namespace mozilla::dom { + +// static +already_AddRefed<SharedWorkerManagerHolder> SharedWorkerManager::Create( + SharedWorkerService* aService, nsIEventTarget* aPBackgroundEventTarget, + const RemoteWorkerData& aData, nsIPrincipal* aLoadingPrincipal, + const OriginAttributes& aEffectiveStoragePrincipalAttrs) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<SharedWorkerManager> manager = + new SharedWorkerManager(aPBackgroundEventTarget, aData, aLoadingPrincipal, + aEffectiveStoragePrincipalAttrs); + + RefPtr<SharedWorkerManagerHolder> holder = + new SharedWorkerManagerHolder(manager, aService); + return holder.forget(); +} + +SharedWorkerManager::SharedWorkerManager( + nsIEventTarget* aPBackgroundEventTarget, const RemoteWorkerData& aData, + nsIPrincipal* aLoadingPrincipal, + const OriginAttributes& aEffectiveStoragePrincipalAttrs) + : mPBackgroundEventTarget(aPBackgroundEventTarget), + mLoadingPrincipal(aLoadingPrincipal), + mDomain(aData.domain()), + mEffectiveStoragePrincipalAttrs(aEffectiveStoragePrincipalAttrs), + mResolvedScriptURL(DeserializeURI(aData.resolvedScriptURL())), + mName(aData.name()), + mIsSecureContext(aData.isSecureContext()), + mSuspended(false), + mFrozen(false) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aLoadingPrincipal); +} + +SharedWorkerManager::~SharedWorkerManager() { + NS_ReleaseOnMainThread("SharedWorkerManager::mLoadingPrincipal", + mLoadingPrincipal.forget()); + NS_ProxyRelease("SharedWorkerManager::mRemoteWorkerController", + mPBackgroundEventTarget, mRemoteWorkerController.forget()); +} + +bool SharedWorkerManager::MaybeCreateRemoteWorker( + const RemoteWorkerData& aData, uint64_t aWindowID, + UniqueMessagePortId& aPortIdentifier, base::ProcessId aProcessId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // Creating remote workers may result in creating new processes, but during + // parent shutdown that would add just noise, so better bail out. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return false; + } + + if (!mRemoteWorkerController) { + mRemoteWorkerController = + RemoteWorkerController::Create(aData, this, aProcessId); + if (NS_WARN_IF(!mRemoteWorkerController)) { + return false; + } + } + + if (aWindowID) { + mRemoteWorkerController->AddWindowID(aWindowID); + } + + mRemoteWorkerController->AddPortIdentifier(aPortIdentifier.release()); + return true; +} + +already_AddRefed<SharedWorkerManagerHolder> +SharedWorkerManager::MatchOnMainThread( + SharedWorkerService* aService, const nsACString& aDomain, + nsIURI* aScriptURL, const nsAString& aName, nsIPrincipal* aLoadingPrincipal, + const OriginAttributes& aEffectiveStoragePrincipalAttrs) { + MOZ_ASSERT(NS_IsMainThread()); + + bool urlEquals; + if (NS_FAILED(aScriptURL->Equals(mResolvedScriptURL, &urlEquals))) { + return nullptr; + } + + bool match = + aDomain == mDomain && urlEquals && aName == mName && + // We want to be sure that the window's principal subsumes the + // SharedWorker's loading principal and vice versa. + mLoadingPrincipal->Subsumes(aLoadingPrincipal) && + aLoadingPrincipal->Subsumes(mLoadingPrincipal) && + mEffectiveStoragePrincipalAttrs == aEffectiveStoragePrincipalAttrs; + if (!match) { + return nullptr; + } + + RefPtr<SharedWorkerManagerHolder> holder = + new SharedWorkerManagerHolder(this, aService); + return holder.forget(); +} + +void SharedWorkerManager::AddActor(SharedWorkerParent* aParent) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + MOZ_ASSERT(!mActors.Contains(aParent)); + + mActors.AppendElement(aParent); + + if (mLockCount) { + Unused << aParent->SendNotifyLock(true); + } + + if (mWebTransportCount) { + Unused << aParent->SendNotifyWebTransport(true); + } + + // NB: We don't update our Suspended/Frozen state here, yet. The aParent is + // responsible for doing so from SharedWorkerParent::ManagerCreated. + // XXX But we could avoid iterating all of our actors because if aParent is + // not frozen and we are, we would just need to thaw ourselves. +} + +void SharedWorkerManager::RemoveActor(SharedWorkerParent* aParent) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + MOZ_ASSERT(mActors.Contains(aParent)); + + uint64_t windowID = aParent->WindowID(); + if (windowID) { + mRemoteWorkerController->RemoveWindowID(windowID); + } + + mActors.RemoveElement(aParent); + + if (!mActors.IsEmpty()) { + // Our remaining actors could be all suspended or frozen. + UpdateSuspend(); + UpdateFrozen(); + return; + } +} + +void SharedWorkerManager::Terminate() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActors.IsEmpty()); + MOZ_ASSERT(mHolders.IsEmpty()); + + // mRemoteWorkerController creation can fail. If the creation fails + // mRemoteWorkerController is nullptr and we should stop termination here. + if (!mRemoteWorkerController) { + return; + } + + mRemoteWorkerController->Terminate(); + mRemoteWorkerController = nullptr; +} + +void SharedWorkerManager::UpdateSuspend() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mRemoteWorkerController); + + uint32_t suspended = 0; + + for (SharedWorkerParent* actor : mActors) { + if (actor->IsSuspended()) { + ++suspended; + } + } + + // Call Suspend only when all of our actors' windows are suspended and call + // Resume only when one of them resumes. + if ((mSuspended && suspended == mActors.Length()) || + (!mSuspended && suspended != mActors.Length())) { + return; + } + + if (!mSuspended) { + mSuspended = true; + mRemoteWorkerController->Suspend(); + } else { + mSuspended = false; + mRemoteWorkerController->Resume(); + } +} + +void SharedWorkerManager::UpdateFrozen() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mRemoteWorkerController); + + uint32_t frozen = 0; + + for (SharedWorkerParent* actor : mActors) { + if (actor->IsFrozen()) { + ++frozen; + } + } + + // Similar to UpdateSuspend, above, we only want to be frozen when all of our + // actors are frozen. + if ((mFrozen && frozen == mActors.Length()) || + (!mFrozen && frozen != mActors.Length())) { + return; + } + + if (!mFrozen) { + mFrozen = true; + mRemoteWorkerController->Freeze(); + } else { + mFrozen = false; + mRemoteWorkerController->Thaw(); + } +} + +bool SharedWorkerManager::IsSecureContext() const { return mIsSecureContext; } + +void SharedWorkerManager::CreationFailed() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + for (SharedWorkerParent* actor : mActors) { + Unused << actor->SendError(NS_ERROR_FAILURE); + } +} + +void SharedWorkerManager::CreationSucceeded() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + // Nothing to do here. +} + +void SharedWorkerManager::ErrorReceived(const ErrorValue& aValue) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + for (SharedWorkerParent* actor : mActors) { + Unused << actor->SendError(aValue); + } +} + +void SharedWorkerManager::LockNotified(bool aCreated) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT_IF(!aCreated, mLockCount > 0); + + mLockCount += aCreated ? 1 : -1; + + // Notify only when we either: + // 1. Got a new lock when nothing were there + // 2. Lost all locks + if ((aCreated && mLockCount == 1) || !mLockCount) { + for (SharedWorkerParent* actor : mActors) { + Unused << actor->SendNotifyLock(aCreated); + } + } +}; + +void SharedWorkerManager::WebTransportNotified(bool aCreated) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT_IF(!aCreated, mWebTransportCount > 0); + + mWebTransportCount += aCreated ? 1 : -1; + + // Notify only when we either: + // 1. Got a first WebTransport + // 2. The last WebTransport goes away + if ((aCreated && mWebTransportCount == 1) || mWebTransportCount == 0) { + for (SharedWorkerParent* actor : mActors) { + Unused << actor->SendNotifyWebTransport(aCreated); + } + } +}; + +void SharedWorkerManager::Terminated() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + for (SharedWorkerParent* actor : mActors) { + Unused << actor->SendTerminate(); + } +} + +void SharedWorkerManager::RegisterHolder(SharedWorkerManagerHolder* aHolder) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aHolder); + MOZ_ASSERT(!mHolders.Contains(aHolder)); + + mHolders.AppendElement(aHolder); +} + +void SharedWorkerManager::UnregisterHolder(SharedWorkerManagerHolder* aHolder) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aHolder); + MOZ_ASSERT(mHolders.Contains(aHolder)); + + mHolders.RemoveElement(aHolder); + + if (!mHolders.IsEmpty()) { + return; + } + + // Time to go. + + aHolder->Service()->RemoveWorkerManagerOnMainThread(this); + + RefPtr<SharedWorkerManager> self = this; + mPBackgroundEventTarget->Dispatch( + NS_NewRunnableFunction( + "SharedWorkerService::RemoveWorkerManagerOnMainThread", + [self]() { self->Terminate(); }), + NS_DISPATCH_NORMAL); +} + +SharedWorkerManagerHolder::SharedWorkerManagerHolder( + SharedWorkerManager* aManager, SharedWorkerService* aService) + : mManager(aManager), mService(aService) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aManager); + MOZ_ASSERT(aService); + + aManager->RegisterHolder(this); +} + +SharedWorkerManagerHolder::~SharedWorkerManagerHolder() { + MOZ_ASSERT(NS_IsMainThread()); + mManager->UnregisterHolder(this); +} + +SharedWorkerManagerWrapper::SharedWorkerManagerWrapper( + already_AddRefed<SharedWorkerManagerHolder> aHolder) + : mHolder(aHolder) { + MOZ_ASSERT(NS_IsMainThread()); +} + +SharedWorkerManagerWrapper::~SharedWorkerManagerWrapper() { + NS_ReleaseOnMainThread("SharedWorkerManagerWrapper::mHolder", + mHolder.forget()); +} + +} // namespace mozilla::dom diff --git a/dom/workers/sharedworkers/SharedWorkerManager.h b/dom/workers/sharedworkers/SharedWorkerManager.h new file mode 100644 index 0000000000..fceabca4d4 --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerManager.h @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SharedWorkerManager_h +#define mozilla_dom_SharedWorkerManager_h + +#include "SharedWorkerParent.h" +#include "mozilla/dom/RemoteWorkerController.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class UniqueMessagePortId; +class RemoteWorkerData; +class SharedWorkerManager; +class SharedWorkerService; + +// Main-thread only object that keeps a manager and the service alive. +// When the last SharedWorkerManagerHolder is released, the corresponding +// manager unregisters itself from the service and terminates the worker. +class SharedWorkerManagerHolder final + : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + public: + NS_INLINE_DECL_REFCOUNTING(SharedWorkerManagerHolder); + + SharedWorkerManagerHolder(SharedWorkerManager* aManager, + SharedWorkerService* aService); + + SharedWorkerManager* Manager() const { return mManager; } + + SharedWorkerService* Service() const { return mService; } + + private: + ~SharedWorkerManagerHolder(); + + const RefPtr<SharedWorkerManager> mManager; + const RefPtr<SharedWorkerService> mService; +}; + +// Thread-safe wrapper for SharedWorkerManagerHolder. +class SharedWorkerManagerWrapper final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedWorkerManagerWrapper); + + explicit SharedWorkerManagerWrapper( + already_AddRefed<SharedWorkerManagerHolder> aHolder); + + SharedWorkerManager* Manager() const { return mHolder->Manager(); } + + private: + ~SharedWorkerManagerWrapper(); + + RefPtr<SharedWorkerManagerHolder> mHolder; +}; + +/** + * PBackground instance that corresponds to a single logical Shared Worker that + * exists somewhere in the process tree. Referenced/owned by multiple + * SharedWorkerParent instances on the PBackground thread. Holds/owns a single + * RemoteWorkerController to interact with the actual shared worker thread, + * wherever it is located. Creates the RemoteWorkerController via + * RemoteWorkerController::Create which uses RemoteWorkerManager::Launch under + * the hood. + */ +class SharedWorkerManager final : public RemoteWorkerObserver { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedWorkerManager, override); + + // Called on main-thread thread methods + + static already_AddRefed<SharedWorkerManagerHolder> Create( + SharedWorkerService* aService, nsIEventTarget* aPBackgroundEventTarget, + const RemoteWorkerData& aData, nsIPrincipal* aLoadingPrincipal, + const OriginAttributes& aEffectiveStoragePrincipalAttrs); + + // Returns a holder if this manager matches. The holder blocks the shutdown of + // the manager. + already_AddRefed<SharedWorkerManagerHolder> MatchOnMainThread( + SharedWorkerService* aService, const nsACString& aDomain, + nsIURI* aScriptURL, const nsAString& aName, + nsIPrincipal* aLoadingPrincipal, + const OriginAttributes& aEffectiveStoragePrincipalAttrs); + + // RemoteWorkerObserver + + void CreationFailed() override; + + void CreationSucceeded() override; + + void ErrorReceived(const ErrorValue& aValue) override; + + void LockNotified(bool aCreated) final; + + void WebTransportNotified(bool aCreated) final; + + void Terminated() override; + + // Called on PBackground thread methods + + bool MaybeCreateRemoteWorker(const RemoteWorkerData& aData, + uint64_t aWindowID, + UniqueMessagePortId& aPortIdentifier, + base::ProcessId aProcessId); + + void AddActor(SharedWorkerParent* aParent); + + void RemoveActor(SharedWorkerParent* aParent); + + void UpdateSuspend(); + + void UpdateFrozen(); + + bool IsSecureContext() const; + + void Terminate(); + + // Called on main-thread only. + + void RegisterHolder(SharedWorkerManagerHolder* aHolder); + + void UnregisterHolder(SharedWorkerManagerHolder* aHolder); + + private: + SharedWorkerManager(nsIEventTarget* aPBackgroundEventTarget, + const RemoteWorkerData& aData, + nsIPrincipal* aLoadingPrincipal, + const OriginAttributes& aEffectiveStoragePrincipalAttrs); + + ~SharedWorkerManager(); + + nsCOMPtr<nsIEventTarget> mPBackgroundEventTarget; + + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + const nsCString mDomain; + const OriginAttributes mEffectiveStoragePrincipalAttrs; + const nsCOMPtr<nsIURI> mResolvedScriptURL; + const nsString mName; + const bool mIsSecureContext; + bool mSuspended; + bool mFrozen; + uint32_t mLockCount = 0; + uint32_t mWebTransportCount = 0; + + // Raw pointers because SharedWorkerParent unregisters itself in + // ActorDestroy(). + nsTArray<CheckedUnsafePtr<SharedWorkerParent>> mActors; + + RefPtr<RemoteWorkerController> mRemoteWorkerController; + + // Main-thread only. Raw Pointers because holders keep the manager alive and + // they unregister themselves in their DTOR. + nsTArray<CheckedUnsafePtr<SharedWorkerManagerHolder>> mHolders; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_SharedWorkerManager_h diff --git a/dom/workers/sharedworkers/SharedWorkerParent.cpp b/dom/workers/sharedworkers/SharedWorkerParent.cpp new file mode 100644 index 0000000000..38e56f5100 --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerParent.cpp @@ -0,0 +1,165 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SharedWorkerParent.h" +#include "SharedWorkerManager.h" +#include "SharedWorkerService.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +SharedWorkerParent::SharedWorkerParent() + : mBackgroundEventTarget(GetCurrentSerialEventTarget()), + mStatus(eInit), + mSuspended(false), + mFrozen(false) { + AssertIsOnBackgroundThread(); +} + +SharedWorkerParent::~SharedWorkerParent() = default; + +void SharedWorkerParent::ActorDestroy(IProtocol::ActorDestroyReason aReason) { + AssertIsOnBackgroundThread(); + + if (mWorkerManagerWrapper) { + mWorkerManagerWrapper->Manager()->RemoveActor(this); + mWorkerManagerWrapper = nullptr; + } +} + +void SharedWorkerParent::Initialize( + const RemoteWorkerData& aData, uint64_t aWindowID, + const MessagePortIdentifier& aPortIdentifier) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mStatus == eInit); + + mWindowID = aWindowID; + + mStatus = ePending; + + RefPtr<SharedWorkerService> service = SharedWorkerService::GetOrCreate(); + MOZ_ASSERT(service); + service->GetOrCreateWorkerManager(this, aData, aWindowID, aPortIdentifier); +} + +IPCResult SharedWorkerParent::RecvClose() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mStatus == ePending || mStatus == eActive); + + mStatus = eClosed; + + if (mWorkerManagerWrapper) { + mWorkerManagerWrapper->Manager()->RemoveActor(this); + mWorkerManagerWrapper = nullptr; + } + + Unused << Send__delete__(this); + return IPC_OK(); +} + +IPCResult SharedWorkerParent::RecvSuspend() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mSuspended); + MOZ_ASSERT(mStatus == ePending || mStatus == eActive); + + mSuspended = true; + + if (mStatus == eActive) { + MOZ_ASSERT(mWorkerManagerWrapper); + mWorkerManagerWrapper->Manager()->UpdateSuspend(); + } + + return IPC_OK(); +} + +IPCResult SharedWorkerParent::RecvResume() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mSuspended); + MOZ_ASSERT(mStatus == ePending || mStatus == eActive); + + mSuspended = false; + + if (mStatus == eActive) { + MOZ_ASSERT(mWorkerManagerWrapper); + mWorkerManagerWrapper->Manager()->UpdateSuspend(); + } + + return IPC_OK(); +} + +IPCResult SharedWorkerParent::RecvFreeze() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mFrozen); + MOZ_ASSERT(mStatus == ePending || mStatus == eActive); + + mFrozen = true; + + if (mStatus == eActive) { + MOZ_ASSERT(mWorkerManagerWrapper); + mWorkerManagerWrapper->Manager()->UpdateFrozen(); + } + + return IPC_OK(); +} + +IPCResult SharedWorkerParent::RecvThaw() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFrozen); + MOZ_ASSERT(mStatus == ePending || mStatus == eActive); + + mFrozen = false; + + if (mStatus == eActive) { + MOZ_ASSERT(mWorkerManagerWrapper); + mWorkerManagerWrapper->Manager()->UpdateFrozen(); + } + + return IPC_OK(); +} + +void SharedWorkerParent::ManagerCreated( + already_AddRefed<SharedWorkerManagerWrapper> aWorkerManagerWrapper) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mWorkerManagerWrapper); + MOZ_ASSERT(mStatus == ePending || mStatus == eClosed); + + RefPtr<SharedWorkerManagerWrapper> wrapper = aWorkerManagerWrapper; + + // Already gone. + if (mStatus == eClosed) { + wrapper->Manager()->RemoveActor(this); + return; + } + + mStatus = eActive; + mWorkerManagerWrapper = wrapper; + + mWorkerManagerWrapper->Manager()->UpdateFrozen(); + mWorkerManagerWrapper->Manager()->UpdateSuspend(); +} + +void SharedWorkerParent::ErrorPropagation(nsresult aError) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(NS_FAILED(aError)); + MOZ_ASSERT(mStatus == ePending || mStatus == eClosed); + + // Already gone. + if (mStatus == eClosed) { + return; + } + + Unused << SendError(aError); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/sharedworkers/SharedWorkerParent.h b/dom/workers/sharedworkers/SharedWorkerParent.h new file mode 100644 index 0000000000..c91e66cc2e --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerParent.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_dom_SharedWorkerParent_h +#define mozilla_dom_dom_SharedWorkerParent_h + +#include "mozilla/dom/PSharedWorkerParent.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +class MessagePortIdentifier; +class RemoteWorkerData; +class SharedWorkerManagerWrapper; + +/** + * PBackground actor that relays life-cycle events (freeze/thaw, suspend/resume, + * close) to the PBackground SharedWorkerManager and relays error/termination + * back to the child. + */ +class SharedWorkerParent final + : public mozilla::dom::PSharedWorkerParent, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedWorkerParent) + + SharedWorkerParent(); + + void Initialize(const RemoteWorkerData& aData, uint64_t aWindowID, + const MessagePortIdentifier& aPortIdentifier); + + void ManagerCreated( + already_AddRefed<SharedWorkerManagerWrapper> aWorkerManagerWrapper); + + void ErrorPropagation(nsresult aError); + + mozilla::ipc::IPCResult RecvClose(); + + mozilla::ipc::IPCResult RecvSuspend(); + + mozilla::ipc::IPCResult RecvResume(); + + mozilla::ipc::IPCResult RecvFreeze(); + + mozilla::ipc::IPCResult RecvThaw(); + + bool IsSuspended() const { return mSuspended; } + + bool IsFrozen() const { return mFrozen; } + + uint64_t WindowID() const { return mWindowID; } + + private: + ~SharedWorkerParent(); + + void ActorDestroy(IProtocol::ActorDestroyReason aReason) override; + + nsCOMPtr<nsIEventTarget> mBackgroundEventTarget; + RefPtr<SharedWorkerManagerWrapper> mWorkerManagerWrapper; + + enum { + eInit, + ePending, + eActive, + eClosed, + } mStatus; + + uint64_t mWindowID; + + bool mSuspended; + bool mFrozen; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_dom_SharedWorkerParent_h diff --git a/dom/workers/sharedworkers/SharedWorkerService.cpp b/dom/workers/sharedworkers/SharedWorkerService.cpp new file mode 100644 index 0000000000..c7e3ca1d4c --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerService.cpp @@ -0,0 +1,261 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SharedWorkerService.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/SharedWorkerManager.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticMutex.h" +#include "nsIPrincipal.h" +#include "nsProxyRelease.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +StaticMutex sSharedWorkerMutex; + +StaticRefPtr<SharedWorkerService> sSharedWorkerService; + +class GetOrCreateWorkerManagerRunnable final : public Runnable { + public: + GetOrCreateWorkerManagerRunnable(SharedWorkerService* aService, + SharedWorkerParent* aActor, + const RemoteWorkerData& aData, + uint64_t aWindowID, + const MessagePortIdentifier& aPortIdentifier) + : Runnable("GetOrCreateWorkerManagerRunnable"), + mBackgroundEventTarget(GetCurrentSerialEventTarget()), + mService(aService), + mActor(aActor), + mData(aData), + mWindowID(aWindowID), + mPortIdentifier(aPortIdentifier) {} + + NS_IMETHOD + Run() { + mService->GetOrCreateWorkerManagerOnMainThread( + mBackgroundEventTarget, mActor, mData, mWindowID, mPortIdentifier); + + return NS_OK; + } + + private: + nsCOMPtr<nsIEventTarget> mBackgroundEventTarget; + RefPtr<SharedWorkerService> mService; + RefPtr<SharedWorkerParent> mActor; + RemoteWorkerData mData; + uint64_t mWindowID; + UniqueMessagePortId mPortIdentifier; +}; + +class WorkerManagerCreatedRunnable final : public Runnable { + public: + WorkerManagerCreatedRunnable( + already_AddRefed<SharedWorkerManagerWrapper> aManagerWrapper, + SharedWorkerParent* aActor, const RemoteWorkerData& aData, + uint64_t aWindowID, UniqueMessagePortId& aPortIdentifier) + : Runnable("WorkerManagerCreatedRunnable"), + mManagerWrapper(aManagerWrapper), + mActor(aActor), + mData(aData), + mWindowID(aWindowID), + mPortIdentifier(std::move(aPortIdentifier)) {} + + NS_IMETHOD + Run() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF( + !mActor->CanSend() || + !mManagerWrapper->Manager()->MaybeCreateRemoteWorker( + mData, mWindowID, mPortIdentifier, mActor->OtherPid()))) { + // If we cannot send, the error won't arrive, but we may log something. + mActor->ErrorPropagation(NS_ERROR_FAILURE); + return NS_OK; + } + + mManagerWrapper->Manager()->AddActor(mActor); + mActor->ManagerCreated(mManagerWrapper.forget()); + return NS_OK; + } + + private: + RefPtr<SharedWorkerManagerWrapper> mManagerWrapper; + RefPtr<SharedWorkerParent> mActor; + RemoteWorkerData mData; + uint64_t mWindowID; + UniqueMessagePortId mPortIdentifier; +}; + +class ErrorPropagationRunnable final : public Runnable { + public: + ErrorPropagationRunnable(SharedWorkerParent* aActor, nsresult aError) + : Runnable("ErrorPropagationRunnable"), mActor(aActor), mError(aError) {} + + NS_IMETHOD + Run() { + AssertIsOnBackgroundThread(); + mActor->ErrorPropagation(mError); + return NS_OK; + } + + private: + RefPtr<SharedWorkerParent> mActor; + nsresult mError; +}; + +} // namespace + +/* static */ +already_AddRefed<SharedWorkerService> SharedWorkerService::GetOrCreate() { + AssertIsOnBackgroundThread(); + + StaticMutexAutoLock lock(sSharedWorkerMutex); + + if (!sSharedWorkerService) { + sSharedWorkerService = new SharedWorkerService(); + // ClearOnShutdown can only be called on main thread + nsresult rv = SchedulerGroup::Dispatch(NS_NewRunnableFunction( + "RegisterSharedWorkerServiceClearOnShutdown", []() { + StaticMutexAutoLock lock(sSharedWorkerMutex); + MOZ_ASSERT(sSharedWorkerService); + ClearOnShutdown(&sSharedWorkerService); + })); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + + RefPtr<SharedWorkerService> instance = sSharedWorkerService; + return instance.forget(); +} + +/* static */ +SharedWorkerService* SharedWorkerService::Get() { + StaticMutexAutoLock lock(sSharedWorkerMutex); + + MOZ_ASSERT(sSharedWorkerService); + return sSharedWorkerService; +} + +void SharedWorkerService::GetOrCreateWorkerManager( + SharedWorkerParent* aActor, const RemoteWorkerData& aData, + uint64_t aWindowID, const MessagePortIdentifier& aPortIdentifier) { + AssertIsOnBackgroundThread(); + + // The real check happens on main-thread. + RefPtr<GetOrCreateWorkerManagerRunnable> r = + new GetOrCreateWorkerManagerRunnable(this, aActor, aData, aWindowID, + aPortIdentifier); + + nsresult rv = SchedulerGroup::Dispatch(r.forget()); + Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void SharedWorkerService::GetOrCreateWorkerManagerOnMainThread( + nsIEventTarget* aBackgroundEventTarget, SharedWorkerParent* aActor, + const RemoteWorkerData& aData, uint64_t aWindowID, + UniqueMessagePortId& aPortIdentifier) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBackgroundEventTarget); + MOZ_ASSERT(aActor); + + auto partitionedPrincipalOrErr = + PrincipalInfoToPrincipal(aData.partitionedPrincipalInfo()); + if (NS_WARN_IF(partitionedPrincipalOrErr.isErr())) { + ErrorPropagationOnMainThread(aBackgroundEventTarget, aActor, + partitionedPrincipalOrErr.unwrapErr()); + return; + } + + auto loadingPrincipalOrErr = + PrincipalInfoToPrincipal(aData.loadingPrincipalInfo()); + if (NS_WARN_IF(loadingPrincipalOrErr.isErr())) { + ErrorPropagationOnMainThread(aBackgroundEventTarget, aActor, + loadingPrincipalOrErr.unwrapErr()); + return; + } + + RefPtr<SharedWorkerManagerHolder> managerHolder; + + nsCOMPtr<nsIPrincipal> loadingPrincipal = loadingPrincipalOrErr.unwrap(); + nsCOMPtr<nsIPrincipal> partitionedPrincipal = + partitionedPrincipalOrErr.unwrap(); + + nsCOMPtr<nsIPrincipal> effectiveStoragePrincipal = partitionedPrincipal; + if (aData.useRegularPrincipal()) { + effectiveStoragePrincipal = loadingPrincipal; + } + + // Let's see if there is already a SharedWorker to share. + nsCOMPtr<nsIURI> resolvedScriptURL = + DeserializeURI(aData.resolvedScriptURL()); + for (SharedWorkerManager* workerManager : mWorkerManagers) { + managerHolder = workerManager->MatchOnMainThread( + this, aData.domain(), resolvedScriptURL, aData.name(), loadingPrincipal, + BasePrincipal::Cast(effectiveStoragePrincipal)->OriginAttributesRef()); + if (managerHolder) { + break; + } + } + + // Let's create a new one. + if (!managerHolder) { + managerHolder = SharedWorkerManager::Create( + this, aBackgroundEventTarget, aData, loadingPrincipal, + BasePrincipal::Cast(effectiveStoragePrincipal)->OriginAttributesRef()); + + mWorkerManagers.AppendElement(managerHolder->Manager()); + } else { + // We are attaching the actor to an existing one. + if (managerHolder->Manager()->IsSecureContext() != + aData.isSecureContext()) { + ErrorPropagationOnMainThread(aBackgroundEventTarget, aActor, + NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + RefPtr<SharedWorkerManagerWrapper> wrapper = + new SharedWorkerManagerWrapper(managerHolder.forget()); + + RefPtr<WorkerManagerCreatedRunnable> r = new WorkerManagerCreatedRunnable( + wrapper.forget(), aActor, aData, aWindowID, aPortIdentifier); + aBackgroundEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void SharedWorkerService::ErrorPropagationOnMainThread( + nsIEventTarget* aBackgroundEventTarget, SharedWorkerParent* aActor, + nsresult aError) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBackgroundEventTarget); + MOZ_ASSERT(aActor); + MOZ_ASSERT(NS_FAILED(aError)); + + RefPtr<ErrorPropagationRunnable> r = + new ErrorPropagationRunnable(aActor, aError); + aBackgroundEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void SharedWorkerService::RemoveWorkerManagerOnMainThread( + SharedWorkerManager* aManager) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aManager); + MOZ_ASSERT(mWorkerManagers.Contains(aManager)); + + mWorkerManagers.RemoveElement(aManager); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/sharedworkers/SharedWorkerService.h b/dom/workers/sharedworkers/SharedWorkerService.h new file mode 100644 index 0000000000..c4671163bd --- /dev/null +++ b/dom/workers/sharedworkers/SharedWorkerService.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SharedWorkerService_h +#define mozilla_dom_SharedWorkerService_h + +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +class nsIEventTarget; + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} + +namespace dom { + +class MessagePortIdentifier; +class RemoteWorkerData; +class SharedWorkerManager; +class SharedWorkerParent; +class UniqueMessagePortId; + +/** + * PBackground service that creates and tracks the per-worker + * SharedWorkerManager instances, allowing rendezvous between SharedWorkerParent + * instances and the SharedWorkerManagers they want to talk to (1:1). + */ +class SharedWorkerService final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedWorkerService); + + // This can be called on PBackground thread only. + static already_AddRefed<SharedWorkerService> GetOrCreate(); + + // The service, if already created, is available on any thread using this + // method. + static SharedWorkerService* Get(); + + // PBackground method only. + void GetOrCreateWorkerManager(SharedWorkerParent* aActor, + const RemoteWorkerData& aData, + uint64_t aWindowID, + const MessagePortIdentifier& aPortIdentifier); + + void GetOrCreateWorkerManagerOnMainThread( + nsIEventTarget* aBackgroundEventTarget, SharedWorkerParent* aActor, + const RemoteWorkerData& aData, uint64_t aWindowID, + UniqueMessagePortId& aPortIdentifier); + + void RemoveWorkerManagerOnMainThread(SharedWorkerManager* aManager); + + private: + SharedWorkerService() = default; + ~SharedWorkerService() = default; + + void ErrorPropagationOnMainThread(nsIEventTarget* aBackgroundEventTarget, + SharedWorkerParent* aActor, + nsresult aError); + + // Touched on main-thread only. + nsTArray<RefPtr<SharedWorkerManager>> mWorkerManagers; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SharedWorkerService_h diff --git a/dom/workers/sharedworkers/moz.build b/dom/workers/sharedworkers/moz.build new file mode 100644 index 0000000000..2b83bc9525 --- /dev/null +++ b/dom/workers/sharedworkers/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "SharedWorker.h", + "SharedWorkerChild.h", + "SharedWorkerManager.h", + "SharedWorkerParent.h", +] + +UNIFIED_SOURCES += [ + "SharedWorker.cpp", + "SharedWorkerChild.cpp", + "SharedWorkerManager.cpp", + "SharedWorkerParent.cpp", + "SharedWorkerService.cpp", +] + +IPDL_SOURCES += [ + "PSharedWorker.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/workers/test/404_server.sjs b/dom/workers/test/404_server.sjs new file mode 100644 index 0000000000..f83281efa8 --- /dev/null +++ b/dom/workers/test/404_server.sjs @@ -0,0 +1,9 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 404, "Not found"); + + // Any valid JS. + if (request.queryString == "js") { + response.setHeader("Content-Type", "text/javascript", false); + response.write("4 + 4"); + } +} diff --git a/dom/workers/test/WorkerDebugger.console_childWorker.js b/dom/workers/test/WorkerDebugger.console_childWorker.js new file mode 100644 index 0000000000..8cee6809e5 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.console_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebugger.console_debugger.js b/dom/workers/test/WorkerDebugger.console_debugger.js new file mode 100644 index 0000000000..a8b2493200 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.console_debugger.js @@ -0,0 +1,49 @@ +"use strict"; + +function ok(a, msg) { + postMessage(JSON.stringify({ type: "status", what: !!a, msg })); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + postMessage(JSON.stringify({ type: "finish" })); +} + +function magic() { + console.log("Hello from the debugger script!"); + + var foo = retrieveConsoleEvents(); + ok(Array.isArray(foo), "We received an array."); + ok(foo.length >= 2, "At least 2 messages."); + + is( + foo[0].arguments[0], + "Can you see this console message?", + "First message ok." + ); + is( + foo[1].arguments[0], + "Can you see this second console message?", + "Second message ok." + ); + + setConsoleEventHandler(function (consoleData) { + is(consoleData.arguments[0], "Random message.", "Random message ok!"); + + // The consoleEventHandler can be null. + setConsoleEventHandler(null); + + finish(); + }); +} + +this.onmessage = function (event) { + switch (event.data) { + case "do magic": + magic(); + break; + } +}; diff --git a/dom/workers/test/WorkerDebugger.console_worker.js b/dom/workers/test/WorkerDebugger.console_worker.js new file mode 100644 index 0000000000..a4d6af2e0f --- /dev/null +++ b/dom/workers/test/WorkerDebugger.console_worker.js @@ -0,0 +1,10 @@ +"use strict"; + +console.log("Can you see this console message?"); +console.warn("Can you see this second console message?"); + +var worker = new Worker("WorkerDebugger.console_childWorker.js"); + +setInterval(function () { + console.log("Random message."); +}, 200); diff --git a/dom/workers/test/WorkerDebugger.initialize_childWorker.js b/dom/workers/test/WorkerDebugger.initialize_childWorker.js new file mode 100644 index 0000000000..a3a6d2bf80 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_childWorker.js @@ -0,0 +1,7 @@ +"use strict"; + +self.onmessage = function () {}; + +// eslint-disable-next-line no-debugger +debugger; +postMessage("worker"); diff --git a/dom/workers/test/WorkerDebugger.initialize_debugger.js b/dom/workers/test/WorkerDebugger.initialize_debugger.js new file mode 100644 index 0000000000..f52e95b159 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_debugger.js @@ -0,0 +1,6 @@ +"use strict"; + +var dbg = new Debugger(global); +dbg.onDebuggerStatement = function (frame) { + frame.eval("postMessage('debugger');"); +}; diff --git a/dom/workers/test/WorkerDebugger.initialize_debugger_es_worker.js b/dom/workers/test/WorkerDebugger.initialize_debugger_es_worker.js new file mode 100644 index 0000000000..d357fca2d3 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_debugger_es_worker.js @@ -0,0 +1,10 @@ +"use strict"; + +// The following check is one possible way to identify +// if this script is loaded as a ES Module or the classic way. +const isLoadedAsEsModule = this != globalThis; + +// We expect the debugger script to always be loaded as classic +if (!isLoadedAsEsModule) { + postMessage("debugger script ran"); +} diff --git a/dom/workers/test/WorkerDebugger.initialize_es_worker.js b/dom/workers/test/WorkerDebugger.initialize_es_worker.js new file mode 100644 index 0000000000..1b9ddda769 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_es_worker.js @@ -0,0 +1,10 @@ +"use strict"; + +// The following check is one possible way to identify +// if this script is loaded as a ES Module or the classic way. +const isLoadedAsEsModule = this != globalThis; + +// Here we expect the worker to be loaded a ES Module +if (isLoadedAsEsModule) { + postMessage("worker"); +} diff --git a/dom/workers/test/WorkerDebugger.initialize_worker.js b/dom/workers/test/WorkerDebugger.initialize_worker.js new file mode 100644 index 0000000000..2317335edc --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_worker.js @@ -0,0 +1,10 @@ +"use strict"; + +var worker = new Worker("WorkerDebugger.initialize_childWorker.js"); +worker.onmessage = function (event) { + postMessage("child:" + event.data); +}; + +// eslint-disable-next-line no-debugger +debugger; +postMessage("worker"); diff --git a/dom/workers/test/WorkerDebugger.postMessage_childWorker.js b/dom/workers/test/WorkerDebugger.postMessage_childWorker.js new file mode 100644 index 0000000000..8cee6809e5 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.postMessage_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebugger.postMessage_debugger.js b/dom/workers/test/WorkerDebugger.postMessage_debugger.js new file mode 100644 index 0000000000..14236a4430 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.postMessage_debugger.js @@ -0,0 +1,9 @@ +"use strict"; + +this.onmessage = function (event) { + switch (event.data) { + case "ping": + postMessage("pong"); + break; + } +}; diff --git a/dom/workers/test/WorkerDebugger.postMessage_worker.js b/dom/workers/test/WorkerDebugger.postMessage_worker.js new file mode 100644 index 0000000000..8ddf6cf865 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.postMessage_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +var worker = new Worker("WorkerDebugger.postMessage_childWorker.js"); diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_debugger.js new file mode 100644 index 0000000000..908c9f3161 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_debugger.js @@ -0,0 +1,9 @@ +"use strict"; + +const SANDBOX_URL = "WorkerDebuggerGlobalScope.createSandbox_sandbox.js"; + +var prototype = { + self: this, +}; +var sandbox = createSandbox(SANDBOX_URL, prototype); +loadSubScript(SANDBOX_URL, sandbox); diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_sandbox.js b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_sandbox.js new file mode 100644 index 0000000000..d2de6de924 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_sandbox.js @@ -0,0 +1,9 @@ +"use strict"; + +self.addEventListener("message", function (event) { + switch (event.data) { + case "ping": + self.postMessage("pong"); + break; + } +}); diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_worker.js new file mode 100644 index 0000000000..8cee6809e5 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js new file mode 100644 index 0000000000..495212066c --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js @@ -0,0 +1,16 @@ +"use strict"; + +function f() { + // eslint-disable-next-line no-debugger + debugger; +} + +self.onmessage = function (event) { + switch (event.data) { + case "ping": + // eslint-disable-next-line no-debugger + debugger; + postMessage("pong"); + break; + } +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_debugger.js new file mode 100644 index 0000000000..d2a1a21e85 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_debugger.js @@ -0,0 +1,29 @@ +"use strict"; + +var frames = []; + +var dbg = new Debugger(global); +dbg.onDebuggerStatement = function (frame) { + frames.push(frame); + postMessage("paused"); + enterEventLoop(); + frames.pop(); + postMessage("resumed"); +}; + +this.onmessage = function (event) { + switch (event.data) { + case "eval": + frames[frames.length - 1].eval("f()"); + postMessage("evalled"); + break; + + case "ping": + postMessage("pong"); + break; + + case "resume": + leaveEventLoop(); + break; + } +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_worker.js new file mode 100644 index 0000000000..7b30109615 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_worker.js @@ -0,0 +1,29 @@ +"use strict"; + +function f() { + // eslint-disable-next-line no-debugger + debugger; +} + +var worker = new Worker( + "WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js" +); + +worker.onmessage = function (event) { + postMessage("child:" + event.data); +}; + +self.onmessage = function (event) { + var message = event.data; + if (message.includes(":")) { + worker.postMessage(message.split(":")[1]); + return; + } + switch (message) { + case "ping": + // eslint-disable-next-line no-debugger + debugger; + postMessage("pong"); + break; + } +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.reportError_childWorker.js b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_childWorker.js new file mode 100644 index 0000000000..0fb4113a24 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_childWorker.js @@ -0,0 +1,5 @@ +"use strict"; + +self.onerror = function () { + postMessage("error"); +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.reportError_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_debugger.js new file mode 100644 index 0000000000..b3d8e5d440 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_debugger.js @@ -0,0 +1,11 @@ +"use strict"; + +this.onmessage = function (event) { + switch (event.data) { + case "report": + reportError("reported"); + break; + case "throw": + throw new Error("thrown"); + } +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.reportError_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_worker.js new file mode 100644 index 0000000000..67ccfc2ca0 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_worker.js @@ -0,0 +1,11 @@ +"use strict"; + +var worker = new Worker("WorkerDebuggerGlobalScope.reportError_childWorker.js"); + +worker.onmessage = function (event) { + postMessage("child:" + event.data); +}; + +self.onerror = function () { + postMessage("error"); +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_debugger.js new file mode 100644 index 0000000000..b2a01d380c --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_debugger.js @@ -0,0 +1,12 @@ +"use strict"; + +this.onmessage = function (event) { + switch (event.data) { + case "ping": + setImmediate(function () { + postMessage("pong1"); + }); + postMessage("pong2"); + break; + } +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_worker.js new file mode 100644 index 0000000000..8cee6809e5 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebuggerManager_childWorker.js b/dom/workers/test/WorkerDebuggerManager_childWorker.js new file mode 100644 index 0000000000..8cee6809e5 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerManager_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebuggerManager_worker.js b/dom/workers/test/WorkerDebuggerManager_worker.js new file mode 100644 index 0000000000..0737d17ebc --- /dev/null +++ b/dom/workers/test/WorkerDebuggerManager_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +var worker = new Worker("WorkerDebuggerManager_childWorker.js"); diff --git a/dom/workers/test/WorkerDebugger_childWorker.js b/dom/workers/test/WorkerDebugger_childWorker.js new file mode 100644 index 0000000000..8cee6809e5 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebugger_frozen_window1.html b/dom/workers/test/WorkerDebugger_frozen_window1.html new file mode 100644 index 0000000000..05d65bbb54 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_window1.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + var worker = new Worker("WorkerDebugger_frozen_worker1.js"); + worker.onmessage = function () { + opener.postMessage("ready", "*"); + }; + </script> + </head> + <body> + This is page 1. + </body> +<html> diff --git a/dom/workers/test/WorkerDebugger_frozen_window2.html b/dom/workers/test/WorkerDebugger_frozen_window2.html new file mode 100644 index 0000000000..3a2445ba54 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_window2.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + var worker = new Worker("WorkerDebugger_frozen_worker2.js"); + worker.onmessage = function () { + opener.postMessage("ready", "*"); + }; + </script> + </head> + <body> + This is page 2. + </body> +<html> diff --git a/dom/workers/test/WorkerDebugger_frozen_worker1.js b/dom/workers/test/WorkerDebugger_frozen_worker1.js new file mode 100644 index 0000000000..371d2c064b --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_worker1.js @@ -0,0 +1,5 @@ +"use strict"; + +onmessage = function () {}; + +postMessage("ready"); diff --git a/dom/workers/test/WorkerDebugger_frozen_worker2.js b/dom/workers/test/WorkerDebugger_frozen_worker2.js new file mode 100644 index 0000000000..371d2c064b --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_worker2.js @@ -0,0 +1,5 @@ +"use strict"; + +onmessage = function () {}; + +postMessage("ready"); diff --git a/dom/workers/test/WorkerDebugger_promise_debugger.js b/dom/workers/test/WorkerDebugger_promise_debugger.js new file mode 100644 index 0000000000..dd4d1bf2c9 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_promise_debugger.js @@ -0,0 +1,34 @@ +"use strict"; + +var self = this; + +self.onmessage = function (event) { + if (event.data !== "resolve") { + return; + } + // This then-handler should be executed inside the top-level event loop, + // within the context of the debugger's global. + Promise.resolve().then(function () { + var dbg = new Debugger(global); + dbg.onDebuggerStatement = function () { + self.onmessage = function (e) { + if (e.data !== "resume") { + return; + } + // This then-handler should be executed inside the nested event loop, + // within the context of the debugger's global. + Promise.resolve().then(function () { + postMessage("resumed"); + leaveEventLoop(); + }); + }; + // Test bug 1392540 where DOM Promises from debugger principal + // where frozen while hitting a worker breakpoint. + Promise.resolve().then(() => { + postMessage("paused"); + }); + enterEventLoop(); + }; + postMessage("resolved"); + }); +}; diff --git a/dom/workers/test/WorkerDebugger_promise_worker.js b/dom/workers/test/WorkerDebugger_promise_worker.js new file mode 100644 index 0000000000..04db24b512 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_promise_worker.js @@ -0,0 +1,26 @@ +"use strict"; + +self.onmessage = function (event) { + if (event.data !== "resolve") { + return; + } + // This then-handler should be executed inside the top-level event loop, + // within the context of the worker's global. + Promise.resolve().then(function () { + self.onmessage = function (e) { + if (e.data !== "pause") { + return; + } + // This then-handler should be executed inside the top-level event loop, + // within the context of the worker's global. Because the debugger + // statement here below enters a nested event loop, the then-handler + // should not be executed until the debugger statement returns. + Promise.resolve().then(function () { + postMessage("resumed"); + }); + // eslint-disable-next-line no-debugger + debugger; + }; + postMessage("resolved"); + }); +}; diff --git a/dom/workers/test/WorkerDebugger_sharedWorker.js b/dom/workers/test/WorkerDebugger_sharedWorker.js new file mode 100644 index 0000000000..037abe6d6d --- /dev/null +++ b/dom/workers/test/WorkerDebugger_sharedWorker.js @@ -0,0 +1,16 @@ +"use strict"; + +self.onconnect = function (event) { + event.ports[0].onmessage = function (e) { + switch (e.data) { + case "close": + close(); + break; + + case "close_loop": + close(); + // Let's loop forever. + while (1) {} + } + }; +}; diff --git a/dom/workers/test/WorkerDebugger_suspended_debugger.js b/dom/workers/test/WorkerDebugger_suspended_debugger.js new file mode 100644 index 0000000000..2ed4e16c44 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_suspended_debugger.js @@ -0,0 +1,6 @@ +"use strict"; + +var dbg = new Debugger(global); +dbg.onDebuggerStatement = function (frame) { + postMessage("debugger"); +}; diff --git a/dom/workers/test/WorkerDebugger_suspended_worker.js b/dom/workers/test/WorkerDebugger_suspended_worker.js new file mode 100644 index 0000000000..0ea27d29e8 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_suspended_worker.js @@ -0,0 +1,7 @@ +"use strict"; + +self.onmessage = function () { + postMessage("worker"); + // eslint-disable-next-line no-debugger + debugger; +}; diff --git a/dom/workers/test/WorkerDebugger_worker.js b/dom/workers/test/WorkerDebugger_worker.js new file mode 100644 index 0000000000..e33eeaa4d3 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_worker.js @@ -0,0 +1,9 @@ +"use strict"; + +var worker = new Worker("WorkerDebugger_childWorker.js"); +self.onmessage = function (event) { + postMessage("child:" + event.data); +}; +// eslint-disable-next-line no-debugger +debugger; +postMessage("worker"); diff --git a/dom/workers/test/WorkerTest.jsm b/dom/workers/test/WorkerTest.jsm new file mode 100644 index 0000000000..702dc9e46a --- /dev/null +++ b/dom/workers/test/WorkerTest.jsm @@ -0,0 +1,15 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var EXPORTED_SYMBOLS = ["WorkerTest"]; + +var WorkerTest = { + go(message, messageCallback, errorCallback) { + let worker = new ChromeWorker("WorkerTest_worker.js"); + worker.onmessage = messageCallback; + worker.onerror = errorCallback; + worker.postMessage(message); + return worker; + }, +}; diff --git a/dom/workers/test/WorkerTest_badworker.js b/dom/workers/test/WorkerTest_badworker.js new file mode 100644 index 0000000000..5fbd8a5e63 --- /dev/null +++ b/dom/workers/test/WorkerTest_badworker.js @@ -0,0 +1 @@ +// doesn't matter what is in here if the URL is bad diff --git a/dom/workers/test/WorkerTest_subworker.js b/dom/workers/test/WorkerTest_subworker.js new file mode 100644 index 0000000000..0022fc5046 --- /dev/null +++ b/dom/workers/test/WorkerTest_subworker.js @@ -0,0 +1,37 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + let chromeURL = event.data.replace( + "test_chromeWorkerJSM.xhtml", + "WorkerTest_badworker.js" + ); + + let mochitestURL = event.data + .replace("test_chromeWorkerJSM.xhtml", "WorkerTest_badworker.js") + .replace( + "chrome://mochitests/content/chrome", + "http://mochi.test:8888/tests" + ); + + // We should be able to XHR to anything we want, including a chrome URL. + let xhr = new XMLHttpRequest(); + xhr.open("GET", mochitestURL, false); + xhr.send(); + + if (!xhr.responseText) { + throw "Can't load script file via XHR!"; + } + + // We shouldn't be able to make a ChromeWorker to a non-chrome URL. + try { + new ChromeWorker(mochitestURL); + } catch (e) { + if (e.name === "SecurityError") { + postMessage("Done"); + return; + } + } + throw "creating a chrome worker with a bad URL should throw a SecurityError"; +}; diff --git a/dom/workers/test/WorkerTest_worker.js b/dom/workers/test/WorkerTest_worker.js new file mode 100644 index 0000000000..53a380be50 --- /dev/null +++ b/dom/workers/test/WorkerTest_worker.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + let worker = new ChromeWorker("WorkerTest_subworker.js"); + worker.onmessage = function (e) { + postMessage(e.data); + }; + worker.postMessage(event.data); +}; diff --git a/dom/workers/test/atob_worker.js b/dom/workers/test/atob_worker.js new file mode 100644 index 0000000000..f55ec77e81 --- /dev/null +++ b/dom/workers/test/atob_worker.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var data = [ + -1, + 0, + 1, + 1.5, + /* null ,*/ undefined, + true, + false, + "foo", + "123456789012345", + "1234567890123456", + "12345678901234567", +]; + +var str = ""; +for (var i = 0; i < 30; i++) { + data.push(str); + str += i % 2 ? "b" : "a"; +} + +onmessage = function (event) { + data.forEach(function (string) { + var encoded = btoa(string); + postMessage({ type: "btoa", value: encoded }); + postMessage({ type: "atob", value: atob(encoded) }); + }); + + var threw; + try { + atob(); + } catch (e) { + threw = true; + } + + if (!threw) { + throw "atob didn't throw when called without an argument!"; + } + threw = false; + + try { + btoa(); + } catch (e) { + threw = true; + } + + if (!threw) { + throw "btoa didn't throw when called without an argument!"; + } + + postMessage({ type: "done" }); +}; diff --git a/dom/workers/test/blank.html b/dom/workers/test/blank.html new file mode 100644 index 0000000000..fcbbdb17e9 --- /dev/null +++ b/dom/workers/test/blank.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Blank</title> + </head> + <body onload="notifyOnload();"> + <script type="application/javascript"> + function notifyOnload() { + opener.postMessage({event: 'load'}, '*'); + } + </script> + </body> +</html> diff --git a/dom/workers/test/browser.toml b/dom/workers/test/browser.toml new file mode 100644 index 0000000000..fb8148a159 --- /dev/null +++ b/dom/workers/test/browser.toml @@ -0,0 +1,53 @@ +[DEFAULT] +support-files = [ + "bug1047663_tab.html", + "bug1047663_worker.sjs", + "head.js", + "!/dom/base/test/file_empty.html", +] + +["browser_WorkerDebugger.initialize.js"] +support-files = [ + "WorkerDebugger.initialize_debugger_es_worker.js", + "WorkerDebugger.initialize_es_worker.js", +] +skip-if = ["!nightly_build"] # to be enabled once ES module in workers is enabled (bug 1812591) + +["browser_bug1047663.js"] + +["browser_bug1104623.js"] +run-if = ["buildapp == 'browser'"] + +["browser_consoleSharedWorkers.js"] +skip-if = ["release_or_beta"] # requires dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled +support-files = [ + "sharedWorker_console.js", + "empty.html", +] + +["browser_fileURL.js"] +support-files = [ + "empty.html", + "empty_worker.js", +] + +["browser_privilegedmozilla_remoteworker.js"] +support-files = [ + "file_service_worker.js", + "file_service_worker_container.html", +] + +["browser_serviceworker_fetch_new_process.js"] +support-files = [ + "file_service_worker_fetch_synthetic.js", + "server_fetch_synthetic.sjs", +] + +["browser_worker_use_counters.js"] +support-files = [ + "file_use_counter_worker.html", + "file_use_counter_worker.js", + "file_use_counter_shared_worker.js", + "file_use_counter_shared_worker_microtask.js", + "file_use_counter_service_worker.js", +] diff --git a/dom/workers/test/browser_WorkerDebugger.initialize.js b/dom/workers/test/browser_WorkerDebugger.initialize.js new file mode 100644 index 0000000000..edc20af4e0 --- /dev/null +++ b/dom/workers/test/browser_WorkerDebugger.initialize.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].getService( + Ci.nsIWorkerDebuggerManager +); + +const BASE_URL = "chrome://mochitests/content/browser/dom/workers/test/"; +const WORKER_URL = BASE_URL + "WorkerDebugger.initialize_es_worker.js"; +const DEBUGGER_URL = + BASE_URL + "WorkerDebugger.initialize_debugger_es_worker.js"; + +add_task(async function test() { + const onDbg = waitForRegister(WORKER_URL); + const worker = new Worker(WORKER_URL, { type: "module" }); + + info("Wait for worker message"); + await new Promise(resolve => (worker.onmessage = resolve)); + + const dbg = await onDbg; + + info("Calling WorkerDebugger::Initialize"); + const onDebuggerScriptEvaluated = new Promise(resolve => { + const listener = { + onMessage(msg) { + is(msg, "debugger script ran"); + dbg.removeListener(listener); + resolve(); + }, + }; + dbg.addListener(listener); + }); + dbg.initialize(DEBUGGER_URL); + ok(true, "dbg.initialize didn't throw"); + + info("Waiting for debugger script to be evaluated and dispatching a message"); + await onDebuggerScriptEvaluated; +}); + +function waitForRegister(url, dbgUrl) { + return new Promise(function (resolve) { + wdm.addListener({ + onRegister(dbg) { + if (dbg.url !== url) { + return; + } + ok(true, "Debugger with url " + url + " should be registered."); + wdm.removeListener(this); + resolve(dbg); + }, + }); + }); +} diff --git a/dom/workers/test/browser_bug1047663.js b/dom/workers/test/browser_bug1047663.js new file mode 100644 index 0000000000..f55f1152f6 --- /dev/null +++ b/dom/workers/test/browser_bug1047663.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const EXAMPLE_URL = "https://example.com/browser/dom/workers/test/"; +const TAB_URL = EXAMPLE_URL + "bug1047663_tab.html"; +const WORKER_URL = EXAMPLE_URL + "bug1047663_worker.sjs"; + +function test() { + waitForExplicitFinish(); + + (async function () { + // Disable rcwn to make cache behavior deterministic. + await SpecialPowers.pushPrefEnv({ + set: [["network.http.rcwn.enabled", false]], + }); + + let tab = await addTab(TAB_URL); + + // Create a worker. Post a message to it, and check the reply. Since the + // server side JavaScript file returns the first source for the first + // request, the reply should be "one". If the reply is correct, terminate + // the worker. + await createWorkerInTab(tab, WORKER_URL); + let message = await postMessageToWorkerInTab(tab, WORKER_URL, "ping"); + is(message, "one"); + await terminateWorkerInTab(tab, WORKER_URL); + + // Create a second worker with the same URL. Post a message to it, and check + // the reply. The server side JavaScript file returns the second source for + // all subsequent requests, but since the cache is still enabled, the reply + // should still be "one". If the reply is correct, terminate the worker. + await createWorkerInTab(tab, WORKER_URL); + message = await postMessageToWorkerInTab(tab, WORKER_URL, "ping"); + is(message, "one"); + await terminateWorkerInTab(tab, WORKER_URL); + + // Disable the cache in this tab. This should also disable the cache for all + // workers in this tab. + await disableCacheInTab(tab); + + // Create a third worker with the same URL. Post a message to it, and check + // the reply. Since the server side JavaScript file returns the second + // source for all subsequent requests, and the cache is now disabled, the + // reply should now be "two". If the reply is correct, terminate the worker. + await createWorkerInTab(tab, WORKER_URL); + message = await postMessageToWorkerInTab(tab, WORKER_URL, "ping"); + is(message, "two"); + await terminateWorkerInTab(tab, WORKER_URL); + + removeTab(tab); + + finish(); + })(); +} diff --git a/dom/workers/test/browser_bug1104623.js b/dom/workers/test/browser_bug1104623.js new file mode 100644 index 0000000000..7dc421b873 --- /dev/null +++ b/dom/workers/test/browser_bug1104623.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function whenBrowserLoaded(aBrowser, aCallback) { + aBrowser.addEventListener( + "load", + function onLoad(event) { + if (event.target == aBrowser.contentDocument) { + aBrowser.removeEventListener("load", onLoad, true); + executeSoon(aCallback); + } + }, + true + ); +} + +function test() { + waitForExplicitFinish(); + + let testURL = + "chrome://mochitests/content/chrome/dom/base/test/file_empty.html"; + + let tab = BrowserTestUtils.addTab(gBrowser, testURL); + gBrowser.selectedTab = tab; + + whenBrowserLoaded(tab.linkedBrowser, function () { + let doc = tab.linkedBrowser.contentDocument; + let contentWin = tab.linkedBrowser.contentWindow; + + let blob = new contentWin.Blob([ + "onmessage = function() { postMessage(true); }", + ]); + ok(blob, "Blob has been created"); + + let blobURL = contentWin.URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new contentWin.Worker(blobURL); + ok(worker, "Worker has been created"); + + worker.onerror = function (error) { + ok(false, "Worker.onerror:" + error.message); + worker.terminate(); + contentWin.URL.revokeObjectURL(blob); + gBrowser.removeTab(tab); + executeSoon(finish); + }; + + worker.onmessage = function () { + ok(true, "Worker.onmessage"); + worker.terminate(); + contentWin.URL.revokeObjectURL(blob); + gBrowser.removeTab(tab); + executeSoon(finish); + }; + + worker.postMessage(true); + }); +} diff --git a/dom/workers/test/browser_consoleSharedWorkers.js b/dom/workers/test/browser_consoleSharedWorkers.js new file mode 100644 index 0000000000..54c1d8a73f --- /dev/null +++ b/dom/workers/test/browser_consoleSharedWorkers.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", + true, + ], + ], + }); + + const testURL = getRootDirectory(gTestPath) + "empty.html"; + let tab = BrowserTestUtils.addTab(gBrowser, testURL); + gBrowser.selectedTab = tab; + + await BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab)); + + let promise = new Promise(resolve => { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + function consoleListener() { + this.onConsoleLogEvent = this.onConsoleLogEvent.bind(this); + ConsoleAPIStorage.addLogEventListener( + this.onConsoleLogEvent, + SpecialPowers.wrap(document).nodePrincipal + ); + Services.obs.addObserver(this, "console-api-profiler"); + } + + var order = 0; + consoleListener.prototype = { + onConsoleLogEvent(aSubject) { + var obj = aSubject.wrappedJSObject; + if (order == 1) { + is( + obj.arguments[0], + "Hello world from a SharedWorker!", + "A message from a SharedWorker \\o/" + ); + is(obj.ID, "sharedWorker_console.js", "The ID is SharedWorker"); + is(obj.innerID, "SharedWorker", "The ID is SharedWorker"); + is(order++, 1, "Then a first log message."); + } else { + is( + obj.arguments[0], + "Here is a SAB", + "A message from a SharedWorker \\o/" + ); + is( + obj.arguments[1].constructor.name, + "SharedArrayBuffer", + "We got a direct reference to the SharedArrayBuffer coming from the worker thread" + ); + is(obj.ID, "sharedWorker_console.js", "The ID is SharedWorker"); + is(obj.innerID, "SharedWorker", "The ID is SharedWorker"); + is(order++, 2, "Then a second log message."); + + ConsoleAPIStorage.removeLogEventListener(this.onConsoleLogEvent); + resolve(); + } + }, + + observe: (aSubject, aTopic) => { + ok(true, "Something has been received"); + + if (aTopic == "console-api-profiler") { + var obj = aSubject.wrappedJSObject; + is( + obj.arguments[0], + "Hello profiling from a SharedWorker!", + "A message from a SharedWorker \\o/" + ); + is(order++, 0, "First a profiler message."); + + Services.obs.removeObserver(cl, "console-api-profiler"); + } + }, + }; + + var cl = new consoleListener(); + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + new content.SharedWorker("sharedWorker_console.js"); + }); + + await promise; + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/workers/test/browser_fileURL.js b/dom/workers/test/browser_fileURL.js new file mode 100644 index 0000000000..9891bdcba9 --- /dev/null +++ b/dom/workers/test/browser_fileURL.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const EMPTY_URL = "/browser/dom/workers/test/empty.html"; +const WORKER_URL = "/browser/dom/workers/test/empty_worker.js"; + +add_task(async function () { + let tab = BrowserTestUtils.addTab( + gBrowser, + "http://mochi.test:8888" + EMPTY_URL + ); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + ["http://example.org" + WORKER_URL], + function (spec) { + return new content.Promise((resolve, reject) => { + let w = new content.window.Worker(spec); + w.onerror = _ => { + resolve(); + }; + w.onmessage = _ => { + reject(); + }; + }); + } + ); + ok( + true, + "The worker is not loaded when the script is from different origin." + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + let tab = BrowserTestUtils.addTab( + gBrowser, + "https://example.org" + EMPTY_URL + ); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + ["http://example.org" + WORKER_URL], + function (spec) { + return new content.Promise((resolve, reject) => { + let w = new content.window.Worker(spec); + w.onerror = _ => { + resolve(); + }; + w.onmessage = _ => { + reject(); + }; + }); + } + ); + ok( + true, + "The worker is not loaded when the script is from different origin." + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/workers/test/browser_privilegedmozilla_remoteworker.js b/dom/workers/test/browser_privilegedmozilla_remoteworker.js new file mode 100644 index 0000000000..573cae5e61 --- /dev/null +++ b/dom/workers/test/browser_privilegedmozilla_remoteworker.js @@ -0,0 +1,116 @@ +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true], + ["browser.tabs.remote.separatedMozillaDomains", "example.org"], + ["dom.ipc.processCount.web", 1], + ["dom.ipc.processCount.privilegedmozilla", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +// This test attempts to verify proper placement of spawned remoteworkers +// by spawning them and then verifying that they were spawned in the expected +// process by way of nsIWorkerDebuggerManager enumeration. +// +// Unfortunately, there's no other way to introspect where a worker global was +// spawned at this time. (devtools just ends up enumerating all workers in all +// processes and we don't want to depend on devtools in this test). +// +// As a result, this test currently only tests situations where it's known that +// a remote worker will be spawned in the same process that is initiating its +// spawning. +// +// This should be enhanced in the future. +add_task(async function test_serviceworker() { + const basePath = "browser/dom/workers/test"; + const pagePath = `${basePath}/file_service_worker_container.html`; + const scriptPath = `${basePath}/file_service_worker.js`; + + Services.ppmm.releaseCachedProcesses(); + + async function runWorkerInProcess() { + function getActiveWorkerURLs() { + const wdm = Cc[ + "@mozilla.org/dom/workers/workerdebuggermanager;1" + ].getService(Ci.nsIWorkerDebuggerManager); + + const workerDebuggerUrls = Array.from( + wdm.getWorkerDebuggerEnumerator() + ).map(wd => { + return wd.url; + }); + + return workerDebuggerUrls; + } + + return new Promise(resolve => { + content.navigator.serviceWorker.ready.then(({ active }) => { + const { port1, port2 } = new content.MessageChannel(); + active.postMessage("webpage->serviceworker", [port2]); + port1.onmessage = evt => { + resolve({ + msg: evt.data, + workerUrls: getActiveWorkerURLs(), + }); + }; + }); + }).then(async res => { + // Unregister the service worker used in this test. + const registration = await content.navigator.serviceWorker.ready; + await registration.unregister(); + return res; + }); + } + + const testCaseList = [ + // TODO: find a reasonable way to test the non-privileged scenario + // (because more than 1 process is usually available and the worker + // can be launched in a different one from the one where the tab + // is running). + /*{ + remoteType: "web", + hostname: "example.com", + },*/ + { + remoteType: "privilegedmozilla", + hostname: `example.org`, + }, + ]; + + for (const testCase of testCaseList) { + const { remoteType, hostname } = testCase; + + info(`Test remote serviceworkers launch selects a ${remoteType} process`); + + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: `https://${hostname}/${pagePath}`, + }); + + is( + tab.linkedBrowser.remoteType, + remoteType, + `Got the expected remoteType for ${hostname} tab` + ); + + const results = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + runWorkerInProcess + ); + + Assert.deepEqual( + results, + { + msg: "serviceworker-reply", + workerUrls: [`https://${hostname}/${scriptPath}`], + }, + `Got the expected results for ${hostname} tab` + ); + + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/dom/workers/test/browser_serviceworker_fetch_new_process.js b/dom/workers/test/browser_serviceworker_fetch_new_process.js new file mode 100644 index 0000000000..ae7d71c222 --- /dev/null +++ b/dom/workers/test/browser_serviceworker_fetch_new_process.js @@ -0,0 +1,405 @@ +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); + +/** + * We choose blob contents that will roundtrip cleanly through the `textContent` + * of our returned HTML page. + */ +const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Set preferences so that opening a page with the origin "example.org" + // will result in a remoteType of "privilegedmozilla" for both the + // page and the ServiceWorker. + ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true], + ["browser.tabs.remote.separatedMozillaDomains", "example.org"], + ["dom.ipc.processCount.privilegedmozilla", 1], + ["dom.ipc.processPrelaunch.enabled", false], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // ServiceWorker worker instances should stay alive until explicitly + // caused to terminate by dropping these timeouts to 0 in + // `waitForWorkerAndProcessShutdown`. + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999], + ], + }); +}); + +function countRemoteType(remoteType) { + return ChromeUtils.getAllDOMProcesses().filter( + p => p.remoteType == remoteType + ).length; +} + +/** + * Helper function to get a list of all current processes and their remote + * types. Note that when in used in a templated literal that it is + * synchronously invoked when the string is evaluated and captures system state + * at that instant. + */ +function debugRemotes() { + return ChromeUtils.getAllDOMProcesses() + .map(p => p.remoteType || "parent") + .join(","); +} + +/** + * Wait for there to be zero processes of the given remoteType. This check is + * considered successful if there are already no processes of the given type + * at this very moment. + */ +async function waitForNoProcessesOfType(remoteType) { + info(`waiting for there to be no ${remoteType} procs`); + await TestUtils.waitForCondition( + () => countRemoteType(remoteType) == 0, + "wait for the worker's process to shutdown" + ); +} + +/** + * Given a ServiceWorkerRegistrationInfo with an active ServiceWorker that + * has no active ExtendableEvents but would otherwise continue running thanks + * to the idle keepalive: + * - Assert that there is a ServiceWorker instance in the given registration's + * active slot. (General invariant check.) + * - Assert that a single process with the given remoteType currently exists. + * (This doesn't mean the SW is alive in that process, though this test + * verifies that via other checks when appropriate.) + * - Induce the worker to shutdown by temporarily dropping the idle timeout to 0 + * and causing the idle timer to be reset due to rapid debugger attach/detach. + * - Wait for the the single process with the given remoteType to go away. + * - Reset the idle timeouts back to their previous high values. + */ +async function waitForWorkerAndProcessShutdown(swRegInfo, remoteType) { + info(`terminating worker and waiting for ${remoteType} procs to shut down`); + ok(swRegInfo.activeWorker, "worker should be in the active slot"); + is( + countRemoteType(remoteType), + 1, + `should have a single ${remoteType} process but have: ${debugRemotes()}` + ); + + // Let's not wait too long for the process to shutdown. + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 0], + ], + }); + + // We need to cause the worker to re-evaluate its idle timeout. The easiest + // way to do this I could think of is to attach and then detach the debugger + // from the active worker. + swRegInfo.activeWorker.attachDebugger(); + await new Promise(resolve => Cu.dispatch(resolve)); + swRegInfo.activeWorker.detachDebugger(); + + // Eventually the length will reach 0, meaning we're done! + await waitForNoProcessesOfType(remoteType); + + is( + countRemoteType(remoteType), + 0, + `processes with remoteType=${remoteType} type should have shut down` + ); + + // Make sure we never kill workers on idle except when this is called. + await SpecialPowers.popPrefEnv(); +} + +async function do_test_sw(host, remoteType, swMode, fileBlob) { + info( + `### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}` + ); + + const prin = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(`https://${host}`), + {} + ); + const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`; + const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`; + + const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + const swRegInfo = await swm.registerForTest(prin, scope, sw); + swRegInfo.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo); + + info( + `service worker registered: ${JSON.stringify({ + principal: swRegInfo.principal.spec, + scope: swRegInfo.scope, + })}` + ); + + // Wait for the worker to install & shut down. + await TestUtils.waitForCondition( + () => swRegInfo.activeWorker, + "wait for the worker to become active" + ); + await waitForWorkerAndProcessShutdown(swRegInfo, remoteType); + + info( + `test navigation interception with mode=${swMode} starting from about:blank` + ); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async browser => { + // NOTE: We intentionally trigger the navigation from content in order to + // make sure frontend doesn't eagerly process-switch for us. + SpecialPowers.spawn( + browser, + [scope, swMode, fileBlob], + // eslint-disable-next-line no-shadow + async (scope, swMode, fileBlob) => { + const pageUrl = `${scope}?mode=${swMode}`; + if (!fileBlob) { + content.location.href = pageUrl; + } else { + const doc = content.document; + const formElem = doc.createElement("form"); + doc.body.appendChild(formElem); + + formElem.action = pageUrl; + formElem.method = "POST"; + formElem.enctype = "multipart/form-data"; + + const fileElem = doc.createElement("input"); + formElem.appendChild(fileElem); + + fileElem.type = "file"; + fileElem.name = "foo"; + + fileElem.mozSetFileArray([fileBlob]); + + formElem.submit(); + } + } + ); + + await BrowserTestUtils.browserLoaded(browser); + + is( + countRemoteType(remoteType), + 1, + `should have spawned a content process with remoteType=${remoteType}` + ); + + const { source, blobContents } = await SpecialPowers.spawn( + browser, + [], + () => { + return { + source: content.document.getElementById("source").textContent, + blobContents: content.document.getElementById("blob").textContent, + }; + } + ); + + is( + source, + swMode === "synthetic" ? "ServiceWorker" : "ServerJS", + "The page contents should come from the right place." + ); + + is( + blobContents, + fileBlob ? TEST_BLOB_CONTENTS : "", + "The request blob contents should be the blob/empty as appropriate." + ); + + // Ensure the worker was loaded in this process. + const workerDebuggerURLs = await SpecialPowers.spawn( + browser, + [sw], + async url => { + if (!content.navigator.serviceWorker.controller) { + throw new Error("document not controlled!"); + } + const wdm = Cc[ + "@mozilla.org/dom/workers/workerdebuggermanager;1" + ].getService(Ci.nsIWorkerDebuggerManager); + + return Array.from(wdm.getWorkerDebuggerEnumerator()) + .map(wd => { + return wd.url; + }) + .filter(swURL => swURL == url); + } + ); + if (remoteType.startsWith("webServiceWorker=")) { + Assert.notDeepEqual( + workerDebuggerURLs, + [sw], + "Isolated workers should not be running in the content child process" + ); + } else { + Assert.deepEqual( + workerDebuggerURLs, + [sw], + "The worker should be running in the correct child process" + ); + } + + // Unregister the ServiceWorker. The registration will continue to control + // `browser` and therefore continue to exist and its worker to continue + // running until the tab is closed. + await SpecialPowers.spawn(browser, [], async () => { + let registration = await content.navigator.serviceWorker.ready; + await registration.unregister(); + }); + } + ); + + // Now that the controlled tab is closed and the registration has been + // removed, the ServiceWorker will be made redundant which will forcibly + // terminate it, which will result in the shutdown of the given content + // process. Wait for that to happen both as a verification and so the next + // test has a sufficiently clean slate. + await waitForNoProcessesOfType(remoteType); +} + +/** + * Create a File-backed blob. This will happen synchronously from the main + * thread, which isn't optimal, but the test blocks on this progress anyways. + * Bug 1669578 has been filed on improving this idiom and avoiding the sync + * writes. + */ +async function makeFileBlob(blobContents) { + const tmpFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + tmpFile.append("test-file-backed-blob.txt"); + tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + tmpFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + outStream.write(blobContents, blobContents.length); + outStream.close(); + + const fileBlob = await File.createFromNsIFile(tmpFile); + return fileBlob; +} + +function getSWTelemetrySums() { + let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService( + Ci.nsITelemetry + ); + let keyedhistograms = telemetry.getSnapshotForKeyedHistograms( + "main", + false + ).parent; + let keyedscalars = telemetry.getSnapshotForKeyedScalars("main", false).parent; + // We're not looking at the distribution of the histograms, just that they changed + return { + SERVICE_WORKER_RUNNING_All: keyedhistograms.SERVICE_WORKER_RUNNING + ? keyedhistograms.SERVICE_WORKER_RUNNING.All.sum + : 0, + SERVICE_WORKER_RUNNING_Fetch: keyedhistograms.SERVICE_WORKER_RUNNING + ? keyedhistograms.SERVICE_WORKER_RUNNING.Fetch.sum + : 0, + SERVICEWORKER_RUNNING_MAX_All: keyedscalars["serviceworker.running_max"] + ? keyedscalars["serviceworker.running_max"].All + : 0, + SERVICEWORKER_RUNNING_MAX_Fetch: keyedscalars["serviceworker.running_max"] + ? keyedscalars["serviceworker.running_max"].Fetch + : 0, + }; +} + +add_task(async function test() { + // Can't test telemetry without this since we may not be on the nightly channel + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + registerCleanupFunction(() => { + Services.telemetry.canRecordExtended = oldCanRecord; + }); + + let initialSums = getSWTelemetrySums(); + + // ## Isolated Privileged Process + // Trigger a straightforward intercepted navigation with no request body that + // returns a synthetic response. + await do_test_sw("example.org", "privilegedmozilla", "synthetic", null); + + // Trigger an intercepted navigation with FormData containing an + // <input type="file"> which will result in the request body containing a + // RemoteLazyInputStream which will be consumed in the content process by the + // ServiceWorker while generating the synthetic response. + const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS); + await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob); + + // Trigger an intercepted navigation with FormData containing an + // <input type="file"> which will result in the request body containing a + // RemoteLazyInputStream which will be relayed back to the parent process + // via direct invocation of fetch() on the event.request but without any + // cloning. + await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob); + + // Same as the above but cloning the request before fetching it. + await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob); + + // ## Fission Isolation + if (Services.appinfo.fissionAutostart) { + // ## ServiceWorker isolation + const isolateUrl = "example.com"; + const isolateRemoteType = `webServiceWorker=https://` + isolateUrl; + await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", null); + await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", fileBlob); + } + let telemetrySums = getSWTelemetrySums(); + info(JSON.stringify(telemetrySums)); + info( + "Initial Running All: " + + initialSums.SERVICE_WORKER_RUNNING_All + + ", Fetch: " + + initialSums.SERVICE_WORKER_RUNNING_Fetch + ); + info( + "Initial Max Running All: " + + initialSums.SERVICEWORKER_RUNNING_MAX_All + + ", Fetch: " + + initialSums.SERVICEWORKER_RUNNING_MAX_Fetch + ); + info( + "Running All: " + + telemetrySums.SERVICE_WORKER_RUNNING_All + + ", Fetch: " + + telemetrySums.SERVICE_WORKER_RUNNING_Fetch + ); + info( + "Max Running All: " + + telemetrySums.SERVICEWORKER_RUNNING_MAX_All + + ", Fetch: " + + telemetrySums.SERVICEWORKER_RUNNING_MAX_Fetch + ); + Assert.greater( + telemetrySums.SERVICE_WORKER_RUNNING_All, + initialSums.SERVICE_WORKER_RUNNING_All, + "ServiceWorker running count changed" + ); + Assert.greater( + telemetrySums.SERVICE_WORKER_RUNNING_Fetch, + initialSums.SERVICE_WORKER_RUNNING_Fetch, + "ServiceWorker running count changed" + ); + // We don't use ok()'s for MAX because MAX may have been set before we + // set canRecordExtended, and if so we won't record a new value unless + // the max increases again. +}); diff --git a/dom/workers/test/browser_worker_use_counters.js b/dom/workers/test/browser_worker_use_counters.js new file mode 100644 index 0000000000..d651da31d8 --- /dev/null +++ b/dom/workers/test/browser_worker_use_counters.js @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const gHttpTestRoot = "https://example.com/browser/dom/workers/test/"; + +function unscream(s) { + // Takes SCREAMINGCASE `s` and returns "Screamingcase". + return s.charAt(0) + s.slice(1).toLowerCase(); +} + +function screamToCamel(s) { + // Takes SCREAMING_CASE `s` and returns "screamingCase". + const pascal = s.split("_").map(unscream).join(""); + return pascal.charAt(0).toLowerCase() + pascal.slice(1); +} + +var check_use_counter_worker = async function ( + use_counter_name, + worker_type, + content_task +) { + info(`checking ${use_counter_name} use counters for ${worker_type} worker`); + + let newTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + gBrowser.selectedTab = newTab; + newTab.linkedBrowser.stop(); + + // Hold on to the current values of the instrumentation we're + // interested in. + await Services.fog.testFlushAllChildren(); + let glean_before = + Glean[`useCounterWorker${unscream(worker_type)}`][ + screamToCamel(use_counter_name) + ].testGetValue(); + let glean_destructions_before = + Glean.useCounter[ + `${worker_type.toLowerCase()}WorkersDestroyed` + ].testGetValue(); + + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + gHttpTestRoot + "file_use_counter_worker.html" + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await content_task(gBrowser.selectedBrowser); + + // Tear down the page. + let tabClosed = BrowserTestUtils.waitForTabClosing(newTab); + gBrowser.removeTab(newTab); + await tabClosed; + + // Grab data again and compare. + // We'd like for this to be synchronous, but use counters are reported on + // worker destruction which we don't directly observe. + // So we check in a quick loop. + await BrowserTestUtils.waitForCondition(async () => { + await Services.fog.testFlushAllChildren(); + return ( + glean_before != + Glean[`useCounterWorker${unscream(worker_type)}`][ + screamToCamel(use_counter_name) + ].testGetValue() + ); + }); + let glean_after = + Glean[`useCounterWorker${unscream(worker_type)}`][ + screamToCamel(use_counter_name) + ].testGetValue(); + let glean_destructions_after = + Glean.useCounter[ + `${worker_type.toLowerCase()}WorkersDestroyed` + ].testGetValue(); + + is( + glean_after, + glean_before + 1, + `Glean counter ${use_counter_name} for ${worker_type} worker is correct.` + ); + // There might be other workers created by prior tests get destroyed during + // this tests. + Assert.greater( + glean_destructions_after, + glean_destructions_before ?? 0, + `Glean ${worker_type} worker counts are correct` + ); +}; + +add_task(async function test_dedicated_worker() { + await check_use_counter_worker("CONSOLE_LOG", "DEDICATED", async browser => { + await ContentTask.spawn(browser, {}, function () { + return new Promise(resolve => { + let worker = new content.Worker("file_use_counter_worker.js"); + worker.onmessage = function (e) { + if (e.data === "DONE") { + worker.terminate(); + resolve(); + } + }; + }); + }); + }); +}); + +add_task(async function test_shared_worker() { + await check_use_counter_worker("CONSOLE_LOG", "SHARED", async browser => { + await ContentTask.spawn(browser, {}, function () { + return new Promise(resolve => { + let worker = new content.SharedWorker( + "file_use_counter_shared_worker.js" + ); + worker.port.onmessage = function (e) { + if (e.data === "DONE") { + resolve(); + } + }; + worker.port.postMessage("RUN"); + }); + }); + }); +}); + +add_task(async function test_shared_worker_microtask() { + await check_use_counter_worker("CONSOLE_LOG", "SHARED", async browser => { + await ContentTask.spawn(browser, {}, function () { + return new Promise(resolve => { + let worker = new content.SharedWorker( + "file_use_counter_shared_worker_microtask.js" + ); + worker.port.onmessage = function (e) { + if (e.data === "DONE") { + resolve(); + } + }; + worker.port.postMessage("RUN"); + }); + }); + }); +}); + +add_task(async function test_service_worker() { + await check_use_counter_worker("CONSOLE_LOG", "SERVICE", async browser => { + await ContentTask.spawn(browser, {}, function () { + let waitForActivated = async function (registration) { + return new Promise(resolve => { + let worker = + registration.installing || + registration.waiting || + registration.active; + if (worker.state === "activated") { + resolve(worker); + return; + } + + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "activated") { + worker.removeEventListener("statechange", onStateChange); + resolve(worker); + } + }); + }); + }; + + return new Promise(resolve => { + content.navigator.serviceWorker + .register("file_use_counter_service_worker.js") + .then(async registration => { + content.navigator.serviceWorker.onmessage = function (e) { + if (e.data === "DONE") { + registration.unregister().then(resolve); + } + }; + let worker = await waitForActivated(registration); + worker.postMessage("RUN"); + }); + }); + }); + }); +}); diff --git a/dom/workers/test/bug1014466_data1.txt b/dom/workers/test/bug1014466_data1.txt new file mode 100644 index 0000000000..a32a4347a4 --- /dev/null +++ b/dom/workers/test/bug1014466_data1.txt @@ -0,0 +1 @@ +1234567890 diff --git a/dom/workers/test/bug1014466_data2.txt b/dom/workers/test/bug1014466_data2.txt new file mode 100644 index 0000000000..4d40154cef --- /dev/null +++ b/dom/workers/test/bug1014466_data2.txt @@ -0,0 +1 @@ +ABCDEFGH diff --git a/dom/workers/test/bug1014466_worker.js b/dom/workers/test/bug1014466_worker.js new file mode 100644 index 0000000000..2161954d2b --- /dev/null +++ b/dom/workers/test/bug1014466_worker.js @@ -0,0 +1,63 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function ok(a, msg) { + postMessage({ type: "status", status: !!a, msg }); +} + +onmessage = function (event) { + function getResponse(url) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.send(); + return xhr.responseText; + } + + const testFile1 = "bug1014466_data1.txt"; + const testFile2 = "bug1014466_data2.txt"; + const testData1 = getResponse(testFile1); + const testData2 = getResponse(testFile2); + + var response_count = 0; + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState == xhr.DONE && xhr.status == 200) { + response_count++; + switch (response_count) { + case 1: + ok(xhr.responseText == testData1, "Check data 1"); + test_data2(); + break; + case 2: + ok(xhr.responseText == testData2, "Check data 2"); + postMessage({ type: "finish" }); + break; + default: + ok(false, "Unexpected response received"); + postMessage({ type: "finish" }); + break; + } + } + }; + xhr.onerror = function (e) { + ok(false, "Got an error event: " + e); + postMessage({ type: "finish" }); + }; + + function test_data1() { + xhr.open("GET", testFile1, true); + xhr.responseType = "text"; + xhr.send(); + } + + function test_data2() { + xhr.abort(); + xhr.open("GET", testFile2, true); + xhr.responseType = "text"; + xhr.send(); + } + + test_data1(); +}; diff --git a/dom/workers/test/bug1020226_frame.html b/dom/workers/test/bug1020226_frame.html new file mode 100644 index 0000000000..039dc9480a --- /dev/null +++ b/dom/workers/test/bug1020226_frame.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1020226 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1020226</title> +</head> +<body> + +<script type="application/javascript"> + var worker = new Worker("bug1020226_worker.js"); + worker.onmessage = function(e) { + window.parent.postMessage("loaded", "*"); + } +</script> +</body> +</html> diff --git a/dom/workers/test/bug1020226_worker.js b/dom/workers/test/bug1020226_worker.js new file mode 100644 index 0000000000..c09abdd7de --- /dev/null +++ b/dom/workers/test/bug1020226_worker.js @@ -0,0 +1,12 @@ +var p = new Promise(function (resolve, reject) { + // This causes a runnable to be queued. + reject(new Error()); + postMessage("loaded"); + + // This prevents that runnable from running until the window calls terminate(), + // at which point the worker goes into the Canceling state and then an + // HoldWorker() is attempted, which fails, which used to result in + // multiple calls to the error reporter, one after the worker's context had + // been GCed. + while (true) {} +}); diff --git a/dom/workers/test/bug1047663_tab.html b/dom/workers/test/bug1047663_tab.html new file mode 100644 index 0000000000..62ab9be7d2 --- /dev/null +++ b/dom/workers/test/bug1047663_tab.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"/> + </head> + <body> + </body> +</html> diff --git a/dom/workers/test/bug1047663_worker.sjs b/dom/workers/test/bug1047663_worker.sjs new file mode 100644 index 0000000000..169ca05f89 --- /dev/null +++ b/dom/workers/test/bug1047663_worker.sjs @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const WORKER_1 = ` + "use strict"; + + self.onmessage = function () { + postMessage("one"); + }; +`; + +const WORKER_2 = ` + "use strict"; + + self.onmessage = function () { + postMessage("two"); + }; +`; + +function handleRequest(request, response) { + let count = getState("count"); + if (count === "") { + count = "1"; + } + + // This header is necessary for the cache to trigger. + response.setHeader("Cache-control", "max-age=3600"); + response.setHeader("Content-Type", "text/javascript", false); + + // If this is the first request, return the first source. + if (count === "1") { + response.write(WORKER_1); + setState("count", "2"); + } + // For all subsequent requests, return the second source. + else { + response.write(WORKER_2); + } +} diff --git a/dom/workers/test/bug1060621_worker.js b/dom/workers/test/bug1060621_worker.js new file mode 100644 index 0000000000..4137e3b06f --- /dev/null +++ b/dom/workers/test/bug1060621_worker.js @@ -0,0 +1,2 @@ +navigator.foobar = 42; +postMessage("done"); diff --git a/dom/workers/test/bug1062920_worker.js b/dom/workers/test/bug1062920_worker.js new file mode 100644 index 0000000000..238bf949ec --- /dev/null +++ b/dom/workers/test/bug1062920_worker.js @@ -0,0 +1,8 @@ +postMessage({ + appCodeName: navigator.appCodeName, + appName: navigator.appName, + appVersion: navigator.appVersion, + platform: navigator.platform, + userAgent: navigator.userAgent, + product: navigator.product, +}); diff --git a/dom/workers/test/bug1063538.sjs b/dom/workers/test/bug1063538.sjs new file mode 100644 index 0000000000..7e4ba33998 --- /dev/null +++ b/dom/workers/test/bug1063538.sjs @@ -0,0 +1,11 @@ +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +function handleRequest(request, response) { + response.processAsync(); + response.write("Hello"); + setTimeout(function () { + response.finish(); + }, 100000); // wait 100 seconds. +} diff --git a/dom/workers/test/bug1063538_worker.js b/dom/workers/test/bug1063538_worker.js new file mode 100644 index 0000000000..33e60f0a16 --- /dev/null +++ b/dom/workers/test/bug1063538_worker.js @@ -0,0 +1,25 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gURL = "http://example.org/tests/dom/workers/test/bug1063538.sjs"; +var xhr = new XMLHttpRequest({ mozAnon: true, mozSystem: true }); +var progressFired = false; + +xhr.onloadend = function (e) { + postMessage({ type: "finish", progressFired }); + self.close(); +}; + +xhr.onprogress = function (e) { + if (e.loaded > 0) { + progressFired = true; + xhr.abort(); + } +}; + +onmessage = function (e) { + xhr.open("GET", gURL, true); + xhr.send(); +}; diff --git a/dom/workers/test/bug1104064_worker.js b/dom/workers/test/bug1104064_worker.js new file mode 100644 index 0000000000..02b802a826 --- /dev/null +++ b/dom/workers/test/bug1104064_worker.js @@ -0,0 +1,10 @@ +onmessage = function () { + var counter = 0; + var id = setInterval(function () { + ++counter; + if (counter == 2) { + clearInterval(id); + postMessage("done"); + } + }, 0); +}; diff --git a/dom/workers/test/bug1132395_sharedWorker.js b/dom/workers/test/bug1132395_sharedWorker.js new file mode 100644 index 0000000000..851c7f7f6c --- /dev/null +++ b/dom/workers/test/bug1132395_sharedWorker.js @@ -0,0 +1,12 @@ +dump("SW created\n"); +onconnect = function (evt) { + dump("SW onconnect\n"); + evt.ports[0].onmessage = function (e) { + dump("SW onmessage\n"); + var blob = new Blob(["123"], { type: "text/plain" }); + dump("SW blob created\n"); + var url = URL.createObjectURL(blob); + dump("SW url created: " + url + "\n"); + evt.ports[0].postMessage("alive \\o/"); + }; +}; diff --git a/dom/workers/test/bug1132924_worker.js b/dom/workers/test/bug1132924_worker.js new file mode 100644 index 0000000000..db67ba323c --- /dev/null +++ b/dom/workers/test/bug1132924_worker.js @@ -0,0 +1,10 @@ +onmessage = function () { + var a = new XMLHttpRequest(); + a.open("GET", "empty.html", false); + a.onreadystatechange = function () { + if (a.readyState == 4) { + postMessage(a.response); + } + }; + a.send(null); +}; diff --git a/dom/workers/test/bug978260_worker.js b/dom/workers/test/bug978260_worker.js new file mode 100644 index 0000000000..126b9c901a --- /dev/null +++ b/dom/workers/test/bug978260_worker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tell the main thread we're here. +postMessage("loaded"); diff --git a/dom/workers/test/bug998474_worker.js b/dom/workers/test/bug998474_worker.js new file mode 100644 index 0000000000..6f6b6a9212 --- /dev/null +++ b/dom/workers/test/bug998474_worker.js @@ -0,0 +1,7 @@ +self.addEventListener("connect", function (e) { + var port = e.ports[0]; + port.onmessage = function (msg) { + // eslint-disable-next-line no-eval + port.postMessage(eval(msg.data)); + }; +}); diff --git a/dom/workers/test/chrome.toml b/dom/workers/test/chrome.toml new file mode 100644 index 0000000000..0b2d68da39 --- /dev/null +++ b/dom/workers/test/chrome.toml @@ -0,0 +1,120 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = [ + "WorkerDebugger.console_childWorker.js", + "WorkerDebugger.console_debugger.js", + "WorkerDebugger.console_worker.js", + "WorkerDebugger.initialize_childWorker.js", + "WorkerDebugger.initialize_debugger.js", + "WorkerDebugger.initialize_worker.js", + "WorkerDebugger.postMessage_childWorker.js", + "WorkerDebugger.postMessage_debugger.js", + "WorkerDebugger.postMessage_worker.js", + "WorkerDebuggerGlobalScope.createSandbox_debugger.js", + "WorkerDebuggerGlobalScope.createSandbox_sandbox.js", + "WorkerDebuggerGlobalScope.createSandbox_worker.js", + "WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js", + "WorkerDebuggerGlobalScope.enterEventLoop_debugger.js", + "WorkerDebuggerGlobalScope.enterEventLoop_worker.js", + "WorkerDebuggerGlobalScope.reportError_childWorker.js", + "WorkerDebuggerGlobalScope.reportError_debugger.js", + "WorkerDebuggerGlobalScope.reportError_worker.js", + "WorkerDebuggerGlobalScope.setImmediate_debugger.js", + "WorkerDebuggerGlobalScope.setImmediate_worker.js", + "WorkerDebuggerManager_childWorker.js", + "WorkerDebuggerManager_worker.js", + "WorkerDebugger_childWorker.js", + "WorkerDebugger_frozen_window1.html", + "WorkerDebugger_frozen_window2.html", + "WorkerDebugger_frozen_worker1.js", + "WorkerDebugger_frozen_worker2.js", + "WorkerDebugger_promise_debugger.js", + "WorkerDebugger_promise_worker.js", + "WorkerDebugger_sharedWorker.js", + "WorkerDebugger_suspended_debugger.js", + "WorkerDebugger_suspended_worker.js", + "WorkerDebugger_worker.js", + "WorkerTest.jsm", + "WorkerTest_subworker.js", + "WorkerTest_worker.js", + "bug1062920_worker.js", + "chromeWorker_subworker.js", + "chromeWorker_worker_submod.sys.mjs", + "chromeWorker_worker.js", + "chromeWorker_worker.sys.mjs", + "dom_worker_helper.js", + "empty.html", + "fileBlobSubWorker_worker.js", + "fileBlob_worker.js", + "filePosting_worker.js", + "fileReadSlice_worker.js", + "fileReaderSyncErrors_worker.js", + "fileReaderSync_worker.js", + "fileSlice_worker.js", + "fileSubWorker_worker.js", + "file_worker.js", + "sharedWorker_privateBrowsing.js", + "sourcemap_header.js", + "sourcemap_header_debugger.js", +] + +["test_WorkerDebugger.initialize.xhtml"] + +["test_WorkerDebugger.postMessage.xhtml"] + +["test_WorkerDebugger.xhtml"] + +["test_WorkerDebuggerGlobalScope.createSandbox.xhtml"] + +["test_WorkerDebuggerGlobalScope.enterEventLoop.xhtml"] + +["test_WorkerDebuggerGlobalScope.reportError.xhtml"] + +["test_WorkerDebuggerGlobalScope.setImmediate.xhtml"] + +["test_WorkerDebuggerManager.xhtml"] + +["test_WorkerDebugger_console.xhtml"] + +["test_WorkerDebugger_frozen.xhtml"] + +["test_WorkerDebugger_promise.xhtml"] + +["test_WorkerDebugger_suspended.xhtml"] + +["test_bug1062920.xhtml"] + +["test_chromeWorker.xhtml"] + +["test_chromeWorkerJSM.xhtml"] + +["test_file.xhtml"] +skip-if = [ + "os == 'linux' && bits == 64 && debug", # Bug 1765445 + "apple_catalina && !debug", # Bug 1765445 +] + +["test_fileBlobPosting.xhtml"] + +["test_fileBlobSubWorker.xhtml"] + +["test_filePosting.xhtml"] + +["test_fileReadSlice.xhtml"] + +["test_fileReaderSync.xhtml"] + +["test_fileReaderSyncErrors.xhtml"] + +["test_fileSlice.xhtml"] + +["test_fileSubWorker.xhtml"] + +["test_readableStream_when_closing.html"] + +["test_sharedWorker_privateBrowsing.html"] + +["test_shutdownCheck.xhtml"] +support-files = ["worker_shutdownCheck.js"] + +["test_sourcemap_header.html"] diff --git a/dom/workers/test/chromeWorker_subworker.js b/dom/workers/test/chromeWorker_subworker.js new file mode 100644 index 0000000000..6e89453150 --- /dev/null +++ b/dom/workers/test/chromeWorker_subworker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + postMessage("Done!"); +}; diff --git a/dom/workers/test/chromeWorker_worker.js b/dom/workers/test/chromeWorker_worker.js new file mode 100644 index 0000000000..2d354b087b --- /dev/null +++ b/dom/workers/test/chromeWorker_worker.js @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +if (!("ctypes" in self)) { + throw "No ctypes!"; +} + +// Go ahead and verify that the ctypes lazy getter actually works. +if (ctypes.toString() != "[object ctypes]") { + throw "Bad ctypes object: " + ctypes.toString(); +} + +onmessage = function (event) { + let worker = new ChromeWorker("chromeWorker_subworker.js"); + worker.onmessage = function (msg) { + postMessage(msg.data); + }; + worker.postMessage(event.data); +}; diff --git a/dom/workers/test/chromeWorker_worker.sys.mjs b/dom/workers/test/chromeWorker_worker.sys.mjs new file mode 100644 index 0000000000..ee96d7829b --- /dev/null +++ b/dom/workers/test/chromeWorker_worker.sys.mjs @@ -0,0 +1,16 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// our onmessage handler lives in the module. +import _ from "./chromeWorker_worker_submod.sys.mjs"; + +if (!("ctypes" in self)) { + throw "No ctypes!"; +} + +// Go ahead and verify that the ctypes lazy getter actually works. +if (ctypes.toString() != "[object ctypes]") { + throw "Bad ctypes object: " + ctypes.toString(); +} diff --git a/dom/workers/test/chromeWorker_worker_submod.sys.mjs b/dom/workers/test/chromeWorker_worker_submod.sys.mjs new file mode 100644 index 0000000000..c2fc29175c --- /dev/null +++ b/dom/workers/test/chromeWorker_worker_submod.sys.mjs @@ -0,0 +1,9 @@ +onmessage = function (event) { + let worker = new ChromeWorker("chromeWorker_subworker.js"); + worker.onmessage = function (msg) { + postMessage(msg.data); + }; + worker.postMessage(event.data); +}; + +export default "go away linter"; diff --git a/dom/workers/test/clearTimeoutsImplicit_worker.js b/dom/workers/test/clearTimeoutsImplicit_worker.js new file mode 100644 index 0000000000..dfb8d0f6ce --- /dev/null +++ b/dom/workers/test/clearTimeoutsImplicit_worker.js @@ -0,0 +1,11 @@ +var count = 0; +function timerFunction() { + if (++count == 30) { + postMessage("ready"); + while (true) {} + } +} + +for (var i = 0; i < 10; i++) { + setInterval(timerFunction, 500); +} diff --git a/dom/workers/test/clearTimeouts_worker.js b/dom/workers/test/clearTimeouts_worker.js new file mode 100644 index 0000000000..6e6198c6b5 --- /dev/null +++ b/dom/workers/test/clearTimeouts_worker.js @@ -0,0 +1,12 @@ +var count = 0; +function timerFunction() { + if (++count == 30) { + close(); + postMessage("ready"); + while (true) {} + } +} + +for (var i = 0; i < 10; i++) { + setInterval(timerFunction, 500); +} diff --git a/dom/workers/test/consoleReplaceable_worker.js b/dom/workers/test/consoleReplaceable_worker.js new file mode 100644 index 0000000000..7e6be8cdd6 --- /dev/null +++ b/dom/workers/test/consoleReplaceable_worker.js @@ -0,0 +1,24 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function (event) { + postMessage({ event: "console exists", status: !!console, last: false }); + var logCalled = false; + console.log = function () { + logCalled = true; + }; + console.log("foo"); + postMessage({ + event: "console.log is replaceable", + status: logCalled, + last: false, + }); + console = 42; + postMessage({ + event: "console is replaceable", + status: console === 42, + last: true, + }); +}; diff --git a/dom/workers/test/console_worker.js b/dom/workers/test/console_worker.js new file mode 100644 index 0000000000..811dc12bae --- /dev/null +++ b/dom/workers/test/console_worker.js @@ -0,0 +1,113 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function (event) { + // TEST: does console exist? + postMessage({ event: "console exists", status: !!console, last: false }); + + postMessage({ + event: "console is the same object", + // eslint-disable-next-line no-self-compare + status: console === console, + last: false, + }); + + postMessage({ event: "trace without function", status: true, last: false }); + + for (var i = 0; i < 10; ++i) { + console.log(i, i, i); + } + + function trace1() { + function trace2() { + function trace3() { + console.trace("trace " + i); + } + trace3(); + } + trace2(); + } + trace1(); + + foobar585956c = function (a) { + console.trace(); + return a + "c"; + }; + + function foobar585956b(a) { + return foobar585956c(a + "b"); + } + + function foobar585956a(omg) { + return foobar585956b(omg + "a"); + } + + function foobar646025(omg) { + console.log(omg, "o", "d"); + } + + function startTimer(timer) { + console.time(timer); + } + + function stopTimer(timer) { + console.timeEnd(timer); + } + + function timeStamp(label) { + console.timeStamp(label); + } + + function testGroups() { + console.groupCollapsed("a", "group"); + console.group("b", "group"); + console.groupEnd(); + } + + foobar585956a("omg"); + foobar646025("omg"); + timeStamp(); + timeStamp("foo"); + testGroups(); + startTimer("foo"); + setTimeout(function () { + stopTimer("foo"); + nextSteps(event); + }, 10); +}; + +function nextSteps(event) { + function namelessTimer() { + console.time(); + console.timeEnd(); + } + + namelessTimer(); + + var str = "Test Message."; + console.log(str); + console.info(str); + console.warn(str); + console.error(str); + console.exception(str); + console.assert(true, str); + console.assert(false, str); + console.profile(str); + console.profileEnd(str); + console.timeStamp(); + console.clear(); + postMessage({ event: "4 messages", status: true, last: false }); + + // Recursive: + if (event.data == true) { + var worker = new Worker("console_worker.js"); + worker.onmessage = function (msg) { + postMessage(msg.data); + }; + worker.postMessage(false); + } else { + postMessage({ event: "bye bye", status: true, last: true }); + } +} diff --git a/dom/workers/test/content_worker.js b/dom/workers/test/content_worker.js new file mode 100644 index 0000000000..d79732b281 --- /dev/null +++ b/dom/workers/test/content_worker.js @@ -0,0 +1,12 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var props = { + ctypes: 1, + OS: 1, +}; +for (var prop in props) { + postMessage({ prop, value: self[prop] }); +} +postMessage({ testfinished: 1 }); diff --git a/dom/workers/test/crashtests/1153636.html b/dom/workers/test/crashtests/1153636.html new file mode 100644 index 0000000000..6ad0d550fd --- /dev/null +++ b/dom/workers/test/crashtests/1153636.html @@ -0,0 +1,5 @@ +<script> + +new Worker("data:text/javascript;charset=UTF-8,self.addEventListener('',function(){},false);"); + +</script> diff --git a/dom/workers/test/crashtests/1158031.html b/dom/workers/test/crashtests/1158031.html new file mode 100644 index 0000000000..6d896bc466 --- /dev/null +++ b/dom/workers/test/crashtests/1158031.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + +function boom() +{ + var w = new Worker("data:text/javascript;charset=UTF-8,"); + w.postMessage(new Blob([], {})); +} + +</script> +<body onload="boom();"></body> diff --git a/dom/workers/test/crashtests/1228456.html b/dom/workers/test/crashtests/1228456.html new file mode 100644 index 0000000000..6d1f0f0a72 --- /dev/null +++ b/dom/workers/test/crashtests/1228456.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + +function boom() +{ + var w; + for (var i = 0; i < 99; ++i) { + w = new SharedWorker("data:text/javascript;charset=UTF-8," + encodeURIComponent(i + ";")); + } + w.port.postMessage(""); +} + +</script> +<body onload="boom();"></body> diff --git a/dom/workers/test/crashtests/1348882.html b/dom/workers/test/crashtests/1348882.html new file mode 100644 index 0000000000..e0288c4ccb --- /dev/null +++ b/dom/workers/test/crashtests/1348882.html @@ -0,0 +1,18 @@ +<!DOCTYPE> +<html> +<head> +<meta charset="UTF-8"> +<script> +function boom() { + let r = new Request("#a#a"); + setTimeout(function(){ + r.formData(); + setTimeout(function(){ + r.blob(); + }, 0); + }, 0); +} +addEventListener("DOMContentLoaded", boom); +</script> +</head> +</html> diff --git a/dom/workers/test/crashtests/1819146.html b/dom/workers/test/crashtests/1819146.html new file mode 100644 index 0000000000..611acb3ed2 --- /dev/null +++ b/dom/workers/test/crashtests/1819146.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<head> + <script id="worker1" type="javascript/worker"> + self.onmessage = async function (e) { + const abort = new AbortController() + const signal = abort.signal + abort.abort() + close() + try { await fetch(undefined, { signal: signal }) } catch (e) {} + await navigator.locks.request("weblock_0", { signal: signal }, () => {}) + await fetch(undefined, { headers: [] }) + } + </script> + <script> + document.addEventListener('DOMContentLoaded', () => { + const blob = new Blob([document.querySelector('#worker1').textContent], { type: 'text/javascript' }) + const worker = new Worker(window.URL.createObjectURL(blob)) + worker.postMessage([], []) + }) + </script> +</head> +</html> diff --git a/dom/workers/test/crashtests/1821066.html b/dom/workers/test/crashtests/1821066.html new file mode 100644 index 0000000000..d654b37768 --- /dev/null +++ b/dom/workers/test/crashtests/1821066.html @@ -0,0 +1,9 @@ +<!DOCTYPE> +<html> +<head> +<meta charset="UTF-8"> +<script> +new Worker("data:text/javascript;charset=UTF-8,import { o } from '404.js'", {type: 'module'}); +</script> +</head> +</html> diff --git a/dom/workers/test/crashtests/779707.html b/dom/workers/test/crashtests/779707.html new file mode 100644 index 0000000000..97a8113dab --- /dev/null +++ b/dom/workers/test/crashtests/779707.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var x = new XMLHttpRequest(); + x.open('GET', "data:text/plain,2", false); + x.send(); + + new Worker("data:text/javascript,3"); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/dom/workers/test/crashtests/943516.html b/dom/workers/test/crashtests/943516.html new file mode 100644 index 0000000000..5f4667850f --- /dev/null +++ b/dom/workers/test/crashtests/943516.html @@ -0,0 +1,10 @@ +<!-- +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<script> +// Using a DOM bindings object as a weak map key should not crash when attempting to +// call the preserve wrapper callback. +new Worker("data:text/javascript;charset=UTF-8,(new WeakMap()).set(self, 0);") +</script> diff --git a/dom/workers/test/crashtests/crashtests.list b/dom/workers/test/crashtests/crashtests.list new file mode 100644 index 0000000000..528f4c8a10 --- /dev/null +++ b/dom/workers/test/crashtests/crashtests.list @@ -0,0 +1,8 @@ +load 779707.html +load 943516.html +load 1153636.html +load 1158031.html +load 1228456.html +load 1348882.html +load 1821066.html +load 1819146.html diff --git a/dom/workers/test/csp_worker.js b/dom/workers/test/csp_worker.js new file mode 100644 index 0000000000..04afa4bda1 --- /dev/null +++ b/dom/workers/test/csp_worker.js @@ -0,0 +1,26 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + if (event.data.do == "eval") { + var res; + try { + // eslint-disable-next-line no-eval + res = eval("40+2"); + } catch (ex) { + res = ex + ""; + } + postMessage(res); + } else if (event.data.do == "nest") { + var worker = new Worker(event.data.uri); + if (--event.data.level) { + worker.postMessage(event.data); + } else { + worker.postMessage({ do: "eval" }); + } + worker.onmessage = e => { + postMessage(e.data); + }; + } +}; diff --git a/dom/workers/test/csp_worker.js^headers^ b/dom/workers/test/csp_worker.js^headers^ new file mode 100644 index 0000000000..7b835bf2a8 --- /dev/null +++ b/dom/workers/test/csp_worker.js^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' blob: ; script-src 'unsafe-eval' diff --git a/dom/workers/test/dom_worker_helper.js b/dom/workers/test/dom_worker_helper.js new file mode 100644 index 0000000000..e61b4793f2 --- /dev/null +++ b/dom/workers/test/dom_worker_helper.js @@ -0,0 +1,157 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].getService( + Ci.nsIWorkerDebuggerManager +); + +const BASE_URL = "chrome://mochitests/content/chrome/dom/workers/test/"; + +var gRemainingTests = 0; + +function waitForWorkerFinish() { + if (gRemainingTests == 0) { + SimpleTest.waitForExplicitFinish(); + } + ++gRemainingTests; +} + +function finish() { + --gRemainingTests; + if (gRemainingTests == 0) { + SimpleTest.finish(); + } +} + +function assertThrows(fun, message) { + let throws = false; + try { + fun(); + } catch (e) { + throws = true; + } + ok(throws, message); +} + +function generateDebuggers() { + return wdm.getWorkerDebuggerEnumerator(); +} + +function findDebugger(url) { + for (let dbg of generateDebuggers()) { + if (dbg.url === url) { + return dbg; + } + } + return null; +} + +function waitForRegister(url, dbgUrl) { + return new Promise(function (resolve) { + wdm.addListener({ + onRegister(dbg) { + if (dbg.url !== url) { + return; + } + ok(true, "Debugger with url " + url + " should be registered."); + wdm.removeListener(this); + if (dbgUrl) { + info("Initializing worker debugger with url " + url + "."); + dbg.initialize(dbgUrl); + } + resolve(dbg); + }, + }); + }); +} + +function waitForUnregister(url) { + return new Promise(function (resolve) { + wdm.addListener({ + onUnregister(dbg) { + if (dbg.url !== url) { + return; + } + ok(true, "Debugger with url " + url + " should be unregistered."); + wdm.removeListener(this); + resolve(); + }, + }); + }); +} + +function waitForDebuggerClose(dbg) { + return new Promise(function (resolve) { + dbg.addListener({ + onClose() { + ok(true, "Debugger should be closed."); + dbg.removeListener(this); + resolve(); + }, + }); + }); +} + +function waitForDebuggerError(dbg) { + return new Promise(function (resolve) { + dbg.addListener({ + onError(filename, lineno, message) { + dbg.removeListener(this); + resolve(new Error(message, filename, lineno)); + }, + }); + }); +} + +function waitForDebuggerMessage(dbg, message) { + return new Promise(function (resolve) { + dbg.addListener({ + onMessage(message1) { + if (message !== message1) { + return; + } + ok(true, "Should receive " + message + " message from debugger."); + dbg.removeListener(this); + resolve(); + }, + }); + }); +} + +function waitForWindowMessage(window, message) { + return new Promise(function (resolve) { + let onmessage = function (event) { + // eslint-disable-next-line no-self-compare + if (event.data !== event.data) { + return; + } + window.removeEventListener("message", onmessage); + resolve(); + }; + window.addEventListener("message", onmessage); + }); +} + +function waitForWorkerMessage(worker, message) { + return new Promise(function (resolve) { + worker.addEventListener("message", function onmessage(event) { + if (event.data !== message) { + return; + } + ok(true, "Should receive " + message + " message from worker."); + worker.removeEventListener("message", onmessage); + resolve(); + }); + }); +} + +function waitForMultiple(promises) { + // There used to be old logic which expects promises to be resolved in + // succession, but where it seems like this was an incorrect assumption. + // Assuming this change sticks, bug 1861778 tracks removing this method + // entirely in favor of Promise.all at the call-sites or transform the callers + // into explicitly documented awaited sequences. + return Promise.all(promises); +} diff --git a/dom/workers/test/dynamicImport_nested.mjs b/dom/workers/test/dynamicImport_nested.mjs new file mode 100644 index 0000000000..688078d803 --- /dev/null +++ b/dom/workers/test/dynamicImport_nested.mjs @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +await import("./dynamicImport_postMessage.mjs"); + +export const message = "first"; diff --git a/dom/workers/test/dynamicImport_postMessage.mjs b/dom/workers/test/dynamicImport_postMessage.mjs new file mode 100644 index 0000000000..ddb9ee0644 --- /dev/null +++ b/dom/workers/test/dynamicImport_postMessage.mjs @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +postMessage("second"); + +export const message = "second"; diff --git a/dom/workers/test/dynamicImport_worker.js b/dom/workers/test/dynamicImport_worker.js new file mode 100644 index 0000000000..90bd422efe --- /dev/null +++ b/dom/workers/test/dynamicImport_worker.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function (event) { + switch (event.data) { + case "start": + import("./dynamicImport_nested.mjs").then(m => postMessage(m.message)); + break; + default: + throw new Error("Bad message: " + event.data); + } +}; diff --git a/dom/workers/test/empty.html b/dom/workers/test/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/workers/test/empty.html diff --git a/dom/workers/test/empty_worker.js b/dom/workers/test/empty_worker.js new file mode 100644 index 0000000000..9dda1c9fd1 --- /dev/null +++ b/dom/workers/test/empty_worker.js @@ -0,0 +1 @@ +postMessage(42); diff --git a/dom/workers/test/errorPropagation_iframe.html b/dom/workers/test/errorPropagation_iframe.html new file mode 100644 index 0000000000..cbaed1778f --- /dev/null +++ b/dom/workers/test/errorPropagation_iframe.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <meta charset="utf-8"> + <body> + <script type="text/javascript"> + var worker; + + function start(workerCount, messageCallback) { + var seenWindowError; + window.onerror = function(message, filename, lineno) { + if (!seenWindowError) { + seenWindowError = true; + messageCallback({ + type: "window", + data: { message, filename, lineno } + }); + return true; + } + return undefined; + }; + + worker = new Worker("errorPropagation_worker.js"); + + worker.onmessage = function(event) { + messageCallback(event.data); + }; + + var seenWorkerError; + worker.onerror = function(event) { + if (!seenWorkerError) { + seenWorkerError = true; + messageCallback({ + type: "worker", + data: { + message: event.message, + filename: event.filename, + lineno: event.lineno + } + }); + event.preventDefault(); + } + }; + + worker.postMessage(workerCount); + } + + function stop() { + worker.terminate(); + } + </script> + </body> +</html> diff --git a/dom/workers/test/errorPropagation_worker.js b/dom/workers/test/errorPropagation_worker.js new file mode 100644 index 0000000000..679181e83a --- /dev/null +++ b/dom/workers/test/errorPropagation_worker.js @@ -0,0 +1,51 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var seenScopeError; +onerror = function (message, filename, lineno) { + if (!seenScopeError) { + seenScopeError = true; + postMessage({ + type: "scope", + data: { message, filename, lineno }, + }); + return true; + } + return undefined; +}; + +onmessage = function (event) { + var workerId = parseInt(event.data); + + if (workerId > 1) { + var worker = new Worker("errorPropagation_worker.js"); + + worker.onmessage = function (msg) { + postMessage(msg.data); + }; + + var seenWorkerError; + worker.onerror = function (error) { + if (!seenWorkerError) { + seenWorkerError = true; + postMessage({ + type: "worker", + data: { + message: error.message, + filename: error.filename, + lineno: error.lineno, + }, + }); + error.preventDefault(); + } + }; + + worker.postMessage(workerId - 1); + return; + } + + var interval = setInterval(function () { + throw new Error("expectedError"); + }, 100); +}; diff --git a/dom/workers/test/errorwarning_worker.js b/dom/workers/test/errorwarning_worker.js new file mode 100644 index 0000000000..39e71f8414 --- /dev/null +++ b/dom/workers/test/errorwarning_worker.js @@ -0,0 +1,46 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function errorHandler() { + postMessage({ type: "error" }); +} + +onmessage = function (event) { + if (event.data.errors) { + try { + // This is an error: + postMessage({ type: "ignore", value: b.aaa }); + } catch (e) { + errorHandler(); + } + } else { + var a = {}; + // This is a warning: + postMessage({ type: "ignore", value: a.foo }); + } + + if (event.data.loop != 0) { + var worker = new Worker("errorwarning_worker.js"); + worker.onerror = errorHandler; + worker.postMessage({ + loop: event.data.loop - 1, + errors: event.data.errors, + }); + + worker.onmessage = function (e) { + postMessage(e.data); + }; + } else { + postMessage({ type: "finish" }); + } +}; + +onerror = errorHandler; +// eslint-disable-next-line no-self-assign +onerror = onerror; +// eslint-disable-next-line no-self-compare +if (!onerror || onerror != onerror) { + throw "onerror wasn't set properly"; +} diff --git a/dom/workers/test/eventDispatch_worker.js b/dom/workers/test/eventDispatch_worker.js new file mode 100644 index 0000000000..57bbc99aa3 --- /dev/null +++ b/dom/workers/test/eventDispatch_worker.js @@ -0,0 +1,84 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const fakeEventType = "foo"; + +function testEventTarget(event) { + if (event.target !== self) { + throw new Error("Event has a bad target!"); + } + if (event.currentTarget) { + throw new Error("Event has a bad currentTarget!"); + } + postMessage(event.data); +} + +addEventListener( + fakeEventType, + function (event) { + throw new Error("Trusted event listener received untrusted event!"); + }, + false, + false +); + +addEventListener( + fakeEventType, + function (event) { + if (event.target !== self || event.currentTarget !== self) { + throw new Error("Fake event has bad target!"); + } + if (event.isTrusted) { + throw new Error("Event should be untrusted!"); + } + event.stopImmediatePropagation(); + postMessage(event.data); + }, + false, + true +); + +addEventListener( + fakeEventType, + function (event) { + throw new Error( + "This shouldn't get called because of stopImmediatePropagation." + ); + }, + false, + true +); + +var count = 0; +onmessage = function (event) { + if (event.target !== self || event.currentTarget !== self) { + throw new Error("Event has bad target!"); + } + + if (!count++) { + var exception; + try { + self.dispatchEvent(event); + } catch (e) { + exception = e; + } + + if (!exception) { + throw new Error("Recursive dispatch didn't fail!"); + } + + event = new MessageEvent(fakeEventType, { + bubbles: event.bubbles, + cancelable: event.cancelable, + data: event.data, + origin: "*", + source: null, + }); + self.dispatchEvent(event); + + return; + } + + setTimeout(testEventTarget, 0, event); +}; diff --git a/dom/workers/test/fibonacci_worker.js b/dom/workers/test/fibonacci_worker.js new file mode 100644 index 0000000000..0efe5a18d9 --- /dev/null +++ b/dom/workers/test/fibonacci_worker.js @@ -0,0 +1,24 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + var n = parseInt(event.data); + + if (n < 2) { + postMessage(n); + return; + } + + var results = []; + for (var i = 1; i <= 2; i++) { + var worker = new Worker("fibonacci_worker.js"); + worker.onmessage = function (msg) { + results.push(parseInt(msg.data)); + if (results.length == 2) { + postMessage(results[0] + results[1]); + } + }; + worker.postMessage(n - i); + } +}; diff --git a/dom/workers/test/fileBlobSubWorker_worker.js b/dom/workers/test/fileBlobSubWorker_worker.js new file mode 100644 index 0000000000..170362b1a4 --- /dev/null +++ b/dom/workers/test/fileBlobSubWorker_worker.js @@ -0,0 +1,17 @@ +/** + * Expects a blob. Returns an object containing the size, type. + * Used to test posting of blob from worker to worker. + */ +onmessage = function (event) { + var worker = new Worker("fileBlob_worker.js"); + + worker.postMessage(event.data); + + worker.onmessage = function (msg) { + postMessage(msg.data); + }; + + worker.onerror = function (error) { + postMessage(undefined); + }; +}; diff --git a/dom/workers/test/fileBlob_worker.js b/dom/workers/test/fileBlob_worker.js new file mode 100644 index 0000000000..fbb50c7c10 --- /dev/null +++ b/dom/workers/test/fileBlob_worker.js @@ -0,0 +1,13 @@ +/** + * Expects a blob. Returns an object containing the size, type. + */ +onmessage = function (event) { + var file = event.data; + + var rtnObj = new Object(); + + rtnObj.size = file.size; + rtnObj.type = file.type; + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/filePosting_worker.js b/dom/workers/test/filePosting_worker.js new file mode 100644 index 0000000000..052d400374 --- /dev/null +++ b/dom/workers/test/filePosting_worker.js @@ -0,0 +1,3 @@ +onmessage = function (event) { + postMessage(event.data); +}; diff --git a/dom/workers/test/fileReadSlice_worker.js b/dom/workers/test/fileReadSlice_worker.js new file mode 100644 index 0000000000..edbad88802 --- /dev/null +++ b/dom/workers/test/fileReadSlice_worker.js @@ -0,0 +1,16 @@ +/** + * Expects an object containing a blob, a start index and an end index + * for slicing. Returns the contents of the blob read as text. + */ +onmessage = function (event) { + var blob = event.data.blob; + var start = event.data.start; + var end = event.data.end; + + var slicedBlob = blob.slice(start, end); + + var fileReader = new FileReaderSync(); + var text = fileReader.readAsText(slicedBlob); + + postMessage(text); +}; diff --git a/dom/workers/test/fileReaderSyncErrors_worker.js b/dom/workers/test/fileReaderSyncErrors_worker.js new file mode 100644 index 0000000000..0ba91469c8 --- /dev/null +++ b/dom/workers/test/fileReaderSyncErrors_worker.js @@ -0,0 +1,82 @@ +/** + * Delegates "is" evaluation back to main thread. + */ +function is(actual, expected, message) { + var rtnObj = new Object(); + rtnObj.actual = actual; + rtnObj.expected = expected; + rtnObj.message = message; + postMessage(rtnObj); +} + +/** + * Tries to write to property. + */ +function writeProperty(file, property) { + var oldValue = file[property]; + file[property] = -1; + is(file[property], oldValue, "Property " + property + " should be readonly."); +} + +/** + * Passes junk arguments to FileReaderSync methods and expects an exception to + * be thrown. + */ +function fileReaderJunkArgument(blob) { + var fileReader = new FileReaderSync(); + + try { + fileReader.readAsBinaryString(blob); + is( + false, + true, + "Should have thrown an exception calling readAsBinaryString." + ); + } catch (ex) { + is(true, true, "Should have thrown an exception."); + } + + try { + fileReader.readAsDataURL(blob); + is(false, true, "Should have thrown an exception calling readAsDataURL."); + } catch (ex) { + is(true, true, "Should have thrown an exception."); + } + + try { + fileReader.readAsArrayBuffer(blob); + is( + false, + true, + "Should have thrown an exception calling readAsArrayBuffer." + ); + } catch (ex) { + is(true, true, "Should have thrown an exception."); + } + + try { + fileReader.readAsText(blob); + is(false, true, "Should have thrown an exception calling readAsText."); + } catch (ex) { + is(true, true, "Should have thrown an exception."); + } +} + +onmessage = function (event) { + var file = event.data; + + // Test read only properties. + writeProperty(file, "size"); + writeProperty(file, "type"); + writeProperty(file, "name"); + + // Bad types. + fileReaderJunkArgument(undefined); + fileReaderJunkArgument(-1); + fileReaderJunkArgument(1); + fileReaderJunkArgument(new Object()); + fileReaderJunkArgument("hello"); + + // Post undefined to indicate that testing has finished. + postMessage(undefined); +}; diff --git a/dom/workers/test/fileReaderSync_worker.js b/dom/workers/test/fileReaderSync_worker.js new file mode 100644 index 0000000000..d5512ef024 --- /dev/null +++ b/dom/workers/test/fileReaderSync_worker.js @@ -0,0 +1,25 @@ +var reader = new FileReaderSync(); + +/** + * Expects an object containing a file and an encoding then uses a + * FileReaderSync to read the file. Returns an object containing the + * file read a binary string, text, url and ArrayBuffer. + */ +onmessage = function (event) { + var file = event.data.file; + var encoding = event.data.encoding; + + var rtnObj = new Object(); + + if (encoding != undefined) { + rtnObj.text = reader.readAsText(file, encoding); + } else { + rtnObj.text = reader.readAsText(file); + } + + rtnObj.bin = reader.readAsBinaryString(file); + rtnObj.url = reader.readAsDataURL(file); + rtnObj.arrayBuffer = reader.readAsArrayBuffer(file); + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/fileSlice_worker.js b/dom/workers/test/fileSlice_worker.js new file mode 100644 index 0000000000..94a283033a --- /dev/null +++ b/dom/workers/test/fileSlice_worker.js @@ -0,0 +1,27 @@ +/** + * Expects an object containing a blob, a start offset, an end offset + * and an optional content type to slice the blob. Returns an object + * containing the size and type of the sliced blob. + */ +onmessage = function (event) { + var blob = event.data.blob; + var start = event.data.start; + var end = event.data.end; + var contentType = event.data.contentType; + + var slicedBlob; + if (contentType == undefined && end == undefined) { + slicedBlob = blob.slice(start); + } else if (contentType == undefined) { + slicedBlob = blob.slice(start, end); + } else { + slicedBlob = blob.slice(start, end, contentType); + } + + var rtnObj = new Object(); + + rtnObj.size = slicedBlob.size; + rtnObj.type = slicedBlob.type; + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/fileSubWorker_worker.js b/dom/workers/test/fileSubWorker_worker.js new file mode 100644 index 0000000000..8a5c002865 --- /dev/null +++ b/dom/workers/test/fileSubWorker_worker.js @@ -0,0 +1,17 @@ +/** + * Expects a file. Returns an object containing the size, type, name and path + * using another worker. Used to test posting of file from worker to worker. + */ +onmessage = function (event) { + var worker = new Worker("file_worker.js"); + + worker.postMessage(event.data); + + worker.onmessage = function (msg) { + postMessage(msg.data); + }; + + worker.onerror = function (error) { + postMessage(undefined); + }; +}; diff --git a/dom/workers/test/file_bug1010784_worker.js b/dom/workers/test/file_bug1010784_worker.js new file mode 100644 index 0000000000..3e88f5cc97 --- /dev/null +++ b/dom/workers/test/file_bug1010784_worker.js @@ -0,0 +1,9 @@ +onmessage = function (event) { + var xhr = new XMLHttpRequest(); + + xhr.open("GET", event.data, false); + xhr.send(); + xhr.open("GET", event.data, false); + xhr.send(); + postMessage("done"); +}; diff --git a/dom/workers/test/file_service_worker.js b/dom/workers/test/file_service_worker.js new file mode 100644 index 0000000000..dd264e340e --- /dev/null +++ b/dom/workers/test/file_service_worker.js @@ -0,0 +1,3 @@ +self.onmessage = evt => { + evt.ports[0].postMessage("serviceworker-reply"); +}; diff --git a/dom/workers/test/file_service_worker_container.html b/dom/workers/test/file_service_worker_container.html new file mode 100644 index 0000000000..625e911adc --- /dev/null +++ b/dom/workers/test/file_service_worker_container.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <script> + (async function () { + await navigator.serviceWorker.register("file_service_worker.js"); + await navigator.serviceWorker.ready; + })(); + </script> + </head> + <body> + Service Worker Container + </body> +</html> diff --git a/dom/workers/test/file_service_worker_fetch_synthetic.js b/dom/workers/test/file_service_worker_fetch_synthetic.js new file mode 100644 index 0000000000..c58aeb294a --- /dev/null +++ b/dom/workers/test/file_service_worker_fetch_synthetic.js @@ -0,0 +1,59 @@ +addEventListener("install", function (evt) { + evt.waitUntil(self.skipWaiting()); +}); + +/** + * Given a multipart/form-data encoded string that we know to have only a single + * part, return the contents of the part. (MIME multipart encoding is too + * exciting to delve into.) + */ +function extractBlobFromMultipartFormData(text) { + const lines = text.split(/\r\n/g); + const firstBlank = lines.indexOf(""); + const foo = lines.slice(firstBlank + 1, -2).join("\n"); + return foo; +} + +self.addEventListener("fetch", event => { + const url = new URL(event.request.url); + const mode = url.searchParams.get("mode"); + + if (mode === "synthetic") { + event.respondWith( + (async () => { + // This works even if there wasn't a body explicitly associated with the + // request. We just get a zero-length string in that case. + const requestBodyContents = await event.request.text(); + const blobContents = + extractBlobFromMultipartFormData(requestBodyContents); + + return new Response( + `<!DOCTYPE HTML><head><meta charset="utf-8"/></head><body> + <h1 id="url">${event.request.url}</h1> + <div id="source">ServiceWorker</div> + <div id="blob">${blobContents}</div> + </body>`, + { headers: { "Content-Type": "text/html" } } + ); + })() + ); + } else if (mode === "fetch") { + event.respondWith(fetch(event.request)); + } else if (mode === "clone") { + // In order for the act of cloning to be interesting, we want the original + // request to remain alive so that any pipes end up having to buffer. + self.originalRequest = event.request; + event.respondWith(fetch(event.request.clone())); + } else { + event.respondWith( + new Response( + `<!DOCTYPE HTML><head><meta charset="utf-8"/></head><body> + <h1 id="error">Bad mode: ${mode}</h1> + <div id="source">ServiceWorker::Error</div> + <div id="blob">No, this is an error.</div> + </body>`, + { headers: { "Content-Type": "text/html" }, status: 400 } + ) + ); + } +}); diff --git a/dom/workers/test/file_use_counter_service_worker.js b/dom/workers/test/file_use_counter_service_worker.js new file mode 100644 index 0000000000..8ee0d2e04a --- /dev/null +++ b/dom/workers/test/file_use_counter_service_worker.js @@ -0,0 +1,9 @@ +onmessage = async function (e) { + if (e.data === "RUN") { + console.log("worker runs"); + await clients.claim(); + clients.matchAll().then(res => { + res.forEach(client => client.postMessage("DONE")); + }); + } +}; diff --git a/dom/workers/test/file_use_counter_shared_worker.js b/dom/workers/test/file_use_counter_shared_worker.js new file mode 100644 index 0000000000..7e4f95af3b --- /dev/null +++ b/dom/workers/test/file_use_counter_shared_worker.js @@ -0,0 +1,10 @@ +onconnect = function (e) { + let port = e.ports[0]; + port.onmessage = function (m) { + if (m.data === "RUN") { + console.log("worker runs"); + port.postMessage("DONE"); + close(); + } + }; +}; diff --git a/dom/workers/test/file_use_counter_shared_worker_microtask.js b/dom/workers/test/file_use_counter_shared_worker_microtask.js new file mode 100644 index 0000000000..b219da2225 --- /dev/null +++ b/dom/workers/test/file_use_counter_shared_worker_microtask.js @@ -0,0 +1,12 @@ +onconnect = function (e) { + let port = e.ports[0]; + port.onmessage = function (m) { + if (m.data === "RUN") { + queueMicrotask(() => { + console.log("worker runs"); + }); + port.postMessage("DONE"); + close(); + } + }; +}; diff --git a/dom/workers/test/file_use_counter_worker.html b/dom/workers/test/file_use_counter_worker.html new file mode 100644 index 0000000000..d034ad6f5f --- /dev/null +++ b/dom/workers/test/file_use_counter_worker.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1202706 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1202706</title> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1202706">Mozilla Bug 1202706</a> +</body> +</html> diff --git a/dom/workers/test/file_use_counter_worker.js b/dom/workers/test/file_use_counter_worker.js new file mode 100644 index 0000000000..12046940a8 --- /dev/null +++ b/dom/workers/test/file_use_counter_worker.js @@ -0,0 +1,2 @@ +console.log("worker runs"); +postMessage("DONE"); diff --git a/dom/workers/test/file_worker.js b/dom/workers/test/file_worker.js new file mode 100644 index 0000000000..6ed8fe1816 --- /dev/null +++ b/dom/workers/test/file_worker.js @@ -0,0 +1,16 @@ +/** + * Expects a file. Returns an object containing the size, type, name and path. + */ +onmessage = function (event) { + var file = event.data; + + var rtnObj = new Object(); + + rtnObj.size = file.size; + rtnObj.type = file.type; + rtnObj.name = file.name; + rtnObj.path = file.path; + rtnObj.lastModified = file.lastModified; + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/foreign.js b/dom/workers/test/foreign.js new file mode 100644 index 0000000000..33c982fa8f --- /dev/null +++ b/dom/workers/test/foreign.js @@ -0,0 +1 @@ +response = "bad"; diff --git a/dom/workers/test/head.js b/dom/workers/test/head.js new file mode 100644 index 0000000000..ce6cebbd06 --- /dev/null +++ b/dom/workers/test/head.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * Add a tab with given `url`. Returns a promise + * that will be resolved when the tab finished loading. + */ +function addTab(url) { + return BrowserTestUtils.openNewForegroundTab(gBrowser, TAB_URL); +} + +/** + * Remove the given `tab`. + */ +function removeTab(tab) { + gBrowser.removeTab(tab); +} + +/** + * Create a worker with the given `url` in the given `tab`. + */ +function createWorkerInTab(tab, url) { + info("Creating worker with url '" + url + "'\n"); + return SpecialPowers.spawn(tab.linkedBrowser, [url], urlChild => { + if (!content._workers) { + content._workers = {}; + } + content._workers[urlChild] = new content.Worker(urlChild); + }); +} + +/** + * Terminate the worker with the given `url` in the given `tab`. + */ +function terminateWorkerInTab(tab, url) { + info("Terminating worker with url '" + url + "'\n"); + return SpecialPowers.spawn(tab.linkedBrowser, [url], urlChild => { + content._workers[urlChild].terminate(); + delete content._workers[urlChild]; + }); +} + +/** + * Post the given `message` to the worker with the given `url` in the given + * `tab`. + */ +function postMessageToWorkerInTab(tab, url, message) { + info("Posting message to worker with url '" + url + "'\n"); + return SpecialPowers.spawn( + tab.linkedBrowser, + [url, message], + (urlChild, messageChild) => { + let worker = content._workers[urlChild]; + worker.postMessage(messageChild); + return new Promise(function (resolve) { + worker.onmessage = function (event) { + worker.onmessage = null; + resolve(event.data); + }; + }); + } + ); +} + +/** + * Disable the cache in the given `tab`. + */ +function disableCacheInTab(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], () => { + content.docShell.defaultLoadFlags = + Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING; + }); +} diff --git a/dom/workers/test/importForeignScripts_worker.js b/dom/workers/test/importForeignScripts_worker.js new file mode 100644 index 0000000000..4e3d65483f --- /dev/null +++ b/dom/workers/test/importForeignScripts_worker.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var target = self; +var response; + +function runTests() { + response = "good"; + try { + importScripts("http://example.org/tests/dom/workers/test/foreign.js"); + } catch (e) { + dump("Got error " + e + " when calling importScripts"); + } + if (response === "good") { + try { + importScripts("redirect_to_foreign.sjs"); + } catch (e) { + dump("Got error " + e + " when calling importScripts"); + } + } + target.postMessage(response); + + // Now, test a nested worker. + if (location.search !== "?nested") { + var worker = new Worker("importForeignScripts_worker.js?nested"); + + worker.onmessage = function (e) { + target.postMessage(e.data); + target.postMessage("finish"); + }; + + worker.onerror = function () { + target.postMessage("nested worker error"); + }; + + worker.postMessage("start"); + } +} + +onmessage = function (e) { + if (e.data === "start") { + runTests(); + } +}; + +onconnect = function (e) { + target = e.ports[0]; + e.ports[0].onmessage = function (msg) { + if (msg.data === "start") { + runTests(); + } + }; +}; diff --git a/dom/workers/test/importScripts_3rdParty_worker.js b/dom/workers/test/importScripts_3rdParty_worker.js new file mode 100644 index 0000000000..326d48f77a --- /dev/null +++ b/dom/workers/test/importScripts_3rdParty_worker.js @@ -0,0 +1,88 @@ +const workerURL = + "http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js"; + +onmessage = function (a) { + if (a.data.nested) { + var worker = new Worker(workerURL); + worker.onmessage = function (event) { + postMessage(event.data); + }; + + worker.onerror = function (event) { + event.preventDefault(); + postMessage({ + error: event instanceof ErrorEvent && event.filename == workerURL, + }); + }; + + a.data.nested = false; + worker.postMessage(a.data); + return; + } + + // This first URL will use the same origin of this script. + var sameOriginURL = new URL(a.data.url); + var fileName1 = 42; + + // This is cross-origin URL. + var crossOriginURL = new URL(a.data.url); + crossOriginURL.host = "example.com"; + crossOriginURL.port = 80; + var fileName2 = 42; + + if (a.data.test == "none") { + importScripts(crossOriginURL.href); + return; + } + + try { + importScripts(sameOriginURL.href); + } catch (e) { + if (!(e instanceof SyntaxError)) { + postMessage({ result: false }); + return; + } + + fileName1 = e.fileName; + } + + if (fileName1 != sameOriginURL.href || !fileName1) { + postMessage({ result: false }); + return; + } + + if (a.data.test == "try") { + var exception; + try { + importScripts(crossOriginURL.href); + } catch (e) { + fileName2 = e.filename; + exception = e; + } + + postMessage({ + result: + fileName2 == workerURL && + exception.name == "NetworkError" && + exception.code == DOMException.NETWORK_ERR, + }); + return; + } + + if (a.data.test == "eventListener") { + addEventListener("error", function (event) { + event.preventDefault(); + postMessage({ + result: event instanceof ErrorEvent && event.filename == workerURL, + }); + }); + } + + if (a.data.test == "onerror") { + onerror = function (...args) { + postMessage({ result: args[1] == workerURL }); + }; + } + + importScripts(crossOriginURL.href); +}; diff --git a/dom/workers/test/importScripts_mixedcontent.html b/dom/workers/test/importScripts_mixedcontent.html new file mode 100644 index 0000000000..2955fdc46e --- /dev/null +++ b/dom/workers/test/importScripts_mixedcontent.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<script> + function ok(cond, msg) { + window.parent.postMessage({status: "ok", data: cond, msg}, "*"); + } + function finish() { + window.parent.postMessage({status: "done"}, "*"); + } + + function testSharedWorker() { + var sw = new SharedWorker("importForeignScripts_worker.js"); + sw.port.onmessage = function(e) { + if (e.data == "finish") { + finish(); + return; + } + ok(e.data === "good", "mixed content for shared workers is correctly blocked"); + }; + + sw.onerror = function() { + ok(false, "Error on shared worker "); + }; + + sw.port.postMessage("start"); + } + + var worker = new Worker("importForeignScripts_worker.js"); + + worker.onmessage = function(e) { + if (e.data == "finish") { + testSharedWorker(); + return; + } + ok(e.data === "good", "mixed content is correctly blocked"); + } + + worker.onerror = function() { + ok(false, "Error on worker"); + } + + worker.postMessage("start"); +</script> diff --git a/dom/workers/test/importScripts_worker.js b/dom/workers/test/importScripts_worker.js new file mode 100644 index 0000000000..ca46c949b9 --- /dev/null +++ b/dom/workers/test/importScripts_worker.js @@ -0,0 +1,62 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Try no args. This shouldn't do anything. +importScripts(); + +// This caused security exceptions in the past, make sure it doesn't! +var constructor = {}.constructor; + +importScripts("importScripts_worker_imported1.js"); + +// Try to call a function defined in the imported script. +importedScriptFunction(); + +function tryBadScripts() { + var badScripts = [ + // Has a syntax error + "importScripts_worker_imported3.js", + // Throws an exception + "importScripts_worker_imported4.js", + // Shouldn't exist! + "http://example.com/non-existing/importScripts_worker_foo.js", + // Not a valid url + "http://notadomain::notafile aword", + ]; + + for (var i = 0; i < badScripts.length; i++) { + var caughtException = false; + var url = badScripts[i]; + try { + importScripts(url); + } catch (e) { + caughtException = true; + } + if (!caughtException) { + throw "Bad script didn't throw exception: " + url; + } + } +} + +const url = "data:text/javascript,const startResponse = 'started';"; +importScripts(url); + +onmessage = function (event) { + switch (event.data) { + case "start": + importScripts("importScripts_worker_imported2.js"); + importedScriptFunction2(); + tryBadScripts(); + postMessage(startResponse); + break; + case "stop": + tryBadScripts(); + postMessage("stopped"); + break; + default: + throw new Error("Bad message: " + event.data); + } +}; + +tryBadScripts(); diff --git a/dom/workers/test/importScripts_worker_imported1.js b/dom/workers/test/importScripts_worker_imported1.js new file mode 100644 index 0000000000..2a1d28c44d --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported1.js @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// This caused security exceptions in the past, make sure it doesn't! +var myConstructor = {}.constructor; + +// Try to call a function defined in the imported script. +function importedScriptFunction() {} diff --git a/dom/workers/test/importScripts_worker_imported2.js b/dom/workers/test/importScripts_worker_imported2.js new file mode 100644 index 0000000000..3f275e237b --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported2.js @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// This caused security exceptions in the past, make sure it doesn't! +var myConstructor2 = {}.constructor; + +// Try to call a function defined in the imported script. +function importedScriptFunction2() {} diff --git a/dom/workers/test/importScripts_worker_imported3.js b/dom/workers/test/importScripts_worker_imported3.js new file mode 100644 index 0000000000..c54be3e5f7 --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported3.js @@ -0,0 +1,6 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Deliberate syntax error, should generate a worker exception! +for (var index = 0; index < 100) {} diff --git a/dom/workers/test/importScripts_worker_imported4.js b/dom/workers/test/importScripts_worker_imported4.js new file mode 100644 index 0000000000..82f8708c59 --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported4.js @@ -0,0 +1,6 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Deliberate throw, should generate a worker exception! +throw new Error("Bah!"); diff --git a/dom/workers/test/instanceof_worker.js b/dom/workers/test/instanceof_worker.js new file mode 100644 index 0000000000..a7f3ab418d --- /dev/null +++ b/dom/workers/test/instanceof_worker.js @@ -0,0 +1,16 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + postMessage({ + event: "XMLHttpRequest", + status: new XMLHttpRequest() instanceof XMLHttpRequest, + last: false, + }); + postMessage({ + event: "XMLHttpRequestUpload", + status: new XMLHttpRequest().upload instanceof XMLHttpRequestUpload, + last: true, + }); +}; diff --git a/dom/workers/test/invalid.js b/dom/workers/test/invalid.js new file mode 100644 index 0000000000..8912b7ee06 --- /dev/null +++ b/dom/workers/test/invalid.js @@ -0,0 +1 @@ +invalid> diff --git a/dom/workers/test/json_worker.js b/dom/workers/test/json_worker.js new file mode 100644 index 0000000000..229ac954c9 --- /dev/null +++ b/dom/workers/test/json_worker.js @@ -0,0 +1,354 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var cyclicalObject = {}; +cyclicalObject.foo = cyclicalObject; + +var cyclicalArray = []; +cyclicalArray.push(cyclicalArray); + +function makeNested(obj, count) { + var innermostobj; + for (var i = 0; i < count; i++) { + obj.foo = { bar: 5 }; + innermostobj = obj.foo; + obj = innermostobj; + } + return innermostobj; +} + +var nestedObject = {}; +makeNested(nestedObject, 100); + +var cyclicalObject = {}; +var innermost = makeNested(cyclicalObject, 1000); +innermost.baz = cyclicalObject; + +var objectWithSaneGetter = {}; +objectWithSaneGetter.__defineGetter__("foo", function () { + return 5; +}); + +// We don't walk prototype chains for cloning so this won't actually do much... +function objectWithSaneGetter2() {} +objectWithSaneGetter2.prototype = { + get foo() { + return 5; + }, +}; + +const throwingGetterThrownString = "bad"; + +var objectWithThrowingGetter = {}; +objectWithThrowingGetter.__defineGetter__("foo", function () { + throw throwingGetterThrownString; +}); + +var typedArrayWithValues = new Int8Array(5); +for (let index in typedArrayWithValues) { + typedArrayWithValues[index] = index; +} + +var typedArrayWithFunBuffer = new Int8Array(4); +for (let index in typedArrayWithFunBuffer) { + typedArrayWithFunBuffer[index] = 255; +} + +var typedArrayWithFunBuffer2 = new Int32Array(typedArrayWithFunBuffer.buffer); + +var xhr = new XMLHttpRequest(); + +var messages = [ + { + type: "object", + value: {}, + jsonValue: "{}", + }, + { + type: "object", + value: { foo: "bar" }, + jsonValue: '{"foo":"bar"}', + }, + { + type: "object", + value: { foo: "bar", foo2: { bee: "bop" } }, + jsonValue: '{"foo":"bar","foo2":{"bee":"bop"}}', + }, + { + type: "object", + value: { foo: "bar", foo2: { bee: "bop" }, foo3: "baz" }, + jsonValue: '{"foo":"bar","foo2":{"bee":"bop"},"foo3":"baz"}', + }, + { + type: "object", + value: { foo: "bar", foo2: [1, 2, 3] }, + jsonValue: '{"foo":"bar","foo2":[1,2,3]}', + }, + { + type: "object", + value: cyclicalObject, + }, + { + type: "object", + value: [null, 2, false, cyclicalObject], + }, + { + type: "object", + value: cyclicalArray, + }, + { + type: "object", + value: { foo: 1, bar: cyclicalArray }, + }, + { + type: "object", + value: nestedObject, + jsonValue: JSON.stringify(nestedObject), + }, + { + type: "object", + value: cyclicalObject, + }, + { + type: "object", + value: objectWithSaneGetter, + jsonValue: '{"foo":5}', + }, + { + type: "object", + value: new objectWithSaneGetter2(), + jsonValue: "{}", + }, + { + type: "object", + value: objectWithThrowingGetter, + exception: true, + }, + { + type: "object", + array: true, + value: [9, 8, 7], + jsonValue: "[9,8,7]", + }, + { + type: "object", + array: true, + value: [9, false, 10.5, { foo: "bar" }], + jsonValue: '[9,false,10.5,{"foo":"bar"}]', + }, + { + type: "object", + shouldEqual: true, + value: null, + }, + { + type: "undefined", + shouldEqual: true, + value: undefined, + }, + { + type: "string", + shouldEqual: true, + value: "Hello", + }, + { + type: "string", + shouldEqual: true, + value: JSON.stringify({ foo: "bar" }), + compareValue: '{"foo":"bar"}', + }, + { + type: "number", + shouldEqual: true, + value: 1, + }, + { + type: "number", + shouldEqual: true, + value: 0, + }, + { + type: "number", + shouldEqual: true, + value: -1, + }, + { + type: "number", + shouldEqual: true, + value: 12345678901234567000, + }, + { + type: "number", + shouldEqual: true, + value: -12345678901234567000, + }, + { + type: "number", + shouldEqual: true, + value: 0.25, + }, + { + type: "number", + shouldEqual: true, + value: -0.25, + }, + { + type: "boolean", + shouldEqual: true, + value: true, + }, + { + type: "boolean", + shouldEqual: true, + value: false, + }, + { + type: "object", + value(foo) { + return "Bad!"; + }, + exception: true, + }, + { + type: "number", + isNaN: true, + value: NaN, + }, + { + type: "number", + isInfinity: true, + value: Infinity, + }, + { + type: "number", + isNegativeInfinity: true, + value: -Infinity, + }, + { + type: "object", + value: new Int32Array(10), + jsonValue: '{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0}', + }, + { + type: "object", + value: new Float32Array(5), + jsonValue: '{"0":0,"1":0,"2":0,"3":0,"4":0}', + }, + { + type: "object", + value: typedArrayWithValues, + jsonValue: '{"0":0,"1":1,"2":2,"3":3,"4":4}', + }, + { + type: "number", + value: typedArrayWithValues[2], + compareValue: 2, + shouldEqual: true, + }, + { + type: "object", + value: typedArrayWithValues.buffer, + jsonValue: "{}", + }, + { + type: "object", + value: typedArrayWithFunBuffer2, + jsonValue: '{"0":-1}', + }, + { + type: "object", + value: { foo: typedArrayWithFunBuffer2 }, + jsonValue: '{"foo":{"0":-1}}', + }, + { + type: "object", + value: [typedArrayWithFunBuffer2], + jsonValue: '[{"0":-1}]', + }, + { + type: "object", + value: { + foo(a) { + alert(b); + }, + }, + exception: true, + }, + { + type: "object", + value: xhr, + exception: true, + }, + { + type: "number", + value: xhr.readyState, + shouldEqual: true, + }, + { + type: "object", + value: { xhr }, + exception: true, + }, + { + type: "object", + value: self, + exception: true, + }, + { + type: "object", + value: { p: ArrayBuffer.prototype }, + exception: true, + }, + { + type: "string", + shouldEqual: true, + value: "testFinished", + }, +]; + +for (let index = 0; index < messages.length; index++) { + var message = messages[index]; + if (message.hasOwnProperty("compareValue")) { + continue; + } + if ( + message.hasOwnProperty("shouldEqual") || + message.hasOwnProperty("shouldCompare") + ) { + message.compareValue = message.value; + } +} + +onmessage = function (event) { + for (let index = 0; index < messages.length; index++) { + var exception = undefined; + + try { + postMessage(messages[index].value); + } catch (e) { + if (e instanceof DOMException) { + if (e.code != DOMException.DATA_CLONE_ERR) { + throw "DOMException with the wrong code: " + e.code; + } + } else if (e != throwingGetterThrownString) { + throw "Exception of the wrong type: " + e; + } + exception = e; + } + + if ( + (exception !== undefined && !messages[index].exception) || + (exception === undefined && messages[index].exception) + ) { + throw ( + "Exception inconsistency [index = " + + index + + ", " + + messages[index].toSource() + + "]: " + + exception + ); + } + } +}; diff --git a/dom/workers/test/loadEncoding_worker.js b/dom/workers/test/loadEncoding_worker.js new file mode 100644 index 0000000000..5e40478445 --- /dev/null +++ b/dom/workers/test/loadEncoding_worker.js @@ -0,0 +1,7 @@ +/* + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +// Bug 484305 - Load workers as UTF-8. +postMessage({ encoding: "KOI8-R", text: "ðÒÉ×ÅÔ" }); +postMessage({ encoding: "UTF-8", text: "Привет" }); diff --git a/dom/workers/test/location_worker.js b/dom/workers/test/location_worker.js new file mode 100644 index 0000000000..2f16364e1f --- /dev/null +++ b/dom/workers/test/location_worker.js @@ -0,0 +1,12 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +for (var string in self.location) { + var value = + typeof self.location[string] === "function" + ? self.location[string]() + : self.location[string]; + postMessage({ string, value }); +} +postMessage({ string: "testfinished" }); diff --git a/dom/workers/test/longThread_worker.js b/dom/workers/test/longThread_worker.js new file mode 100644 index 0000000000..4096354aca --- /dev/null +++ b/dom/workers/test/longThread_worker.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + switch (event.data) { + case "start": + for (var i = 0; i < 10000000; i++) {} + postMessage("done"); + break; + default: + throw "Bad message: " + event.data; + } +}; diff --git a/dom/workers/test/marionette/manifest.toml b/dom/workers/test/marionette/manifest.toml new file mode 100644 index 0000000000..b801ae5719 --- /dev/null +++ b/dom/workers/test/marionette/manifest.toml @@ -0,0 +1,5 @@ +[DEFAULT] + +["test_service_workers_at_startup.py"] + +["test_service_workers_disabled.py"] diff --git a/dom/workers/test/marionette/service_worker_utils.py b/dom/workers/test/marionette/service_worker_utils.py new file mode 100644 index 0000000000..b29789b6f0 --- /dev/null +++ b/dom/workers/test/marionette/service_worker_utils.py @@ -0,0 +1,63 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + + +class MarionetteServiceWorkerTestCase(MarionetteTestCase): + def install_service_worker(self, path): + install_url = self.marionette.absolute_url(path) + self.marionette.navigate(install_url) + Wait(self.marionette).until( + lambda _: self.is_service_worker_registered, + message="Service worker not successfully installed", + ) + + # Wait for the registered service worker to be stored in the Firefox + # profile before restarting the instance to prevent intermittent + # failures (Bug 1665184). + Wait(self.marionette, timeout=10).until( + lambda _: self.profile_serviceworker_txt_exists, + message="Service worker not stored in profile", + ) + + # self.marionette.restart(in_app=True) will restore service workers if + # we don't navigate away before restarting. + self.marionette.navigate("about:blank") + + # Using @property helps avoid the case where missing parens at the call site + # yields an unvarying 'true' value. + @property + def profile_serviceworker_txt_exists(self): + return "serviceworker.txt" in os.listdir(self.marionette.profile_path) + + @property + def is_service_worker_registered(self): + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + """ + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let ssm = Services.scriptSecurityManager; + + let principal = ssm.createContentPrincipalFromOrigin(arguments[0]); + + let serviceWorkers = swm.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (sw.principal.origin == principal.origin) { + return true; + } + } + return false; + """, + script_args=(self.marionette.absolute_url(""),), + ) diff --git a/dom/workers/test/marionette/test_service_workers_at_startup.py b/dom/workers/test/marionette/test_service_workers_at_startup.py new file mode 100644 index 0000000000..14ff32f87f --- /dev/null +++ b/dom/workers/test/marionette/test_service_workers_at_startup.py @@ -0,0 +1,31 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +# Add this directory to the import path. +sys.path.append(os.path.dirname(__file__)) + +from marionette_driver import Wait +from service_worker_utils import MarionetteServiceWorkerTestCase + + +class ServiceWorkerAtStartupTestCase(MarionetteServiceWorkerTestCase): + def setUp(self): + super(ServiceWorkerAtStartupTestCase, self).setUp() + self.install_service_worker("serviceworker/install_serviceworker.html") + + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + super(ServiceWorkerAtStartupTestCase, self).tearDown() + + def test_registered_service_worker_after_restart(self): + self.marionette.restart() + + Wait(self.marionette).until( + lambda _: self.is_service_worker_registered, + message="Service worker not registered after restart", + ) + self.assertTrue(self.is_service_worker_registered) diff --git a/dom/workers/test/marionette/test_service_workers_disabled.py b/dom/workers/test/marionette/test_service_workers_disabled.py new file mode 100644 index 0000000000..deed164242 --- /dev/null +++ b/dom/workers/test/marionette/test_service_workers_disabled.py @@ -0,0 +1,37 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys + +# Add this directory to the import path. +sys.path.append(os.path.dirname(__file__)) + +from service_worker_utils import MarionetteServiceWorkerTestCase + + +class ServiceWorkersDisabledTestCase(MarionetteServiceWorkerTestCase): + def setUp(self): + super(ServiceWorkersDisabledTestCase, self).setUp() + self.install_service_worker("serviceworker/install_serviceworker.html") + + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + super(ServiceWorkersDisabledTestCase, self).tearDown() + + def test_service_workers_disabled_at_startup(self): + # self.marionette.set_pref sets preferences after startup. Using it + # here causes intermittent failures. + self.marionette.instance.profile.set_preferences( + { + "dom.serviceWorkers.enabled": False, + } + ) + + self.marionette.restart() + + self.assertFalse( + self.is_service_worker_registered, + "Service worker registration should have been purged", + ) diff --git a/dom/workers/test/mochitest.toml b/dom/workers/test/mochitest.toml new file mode 100644 index 0000000000..5ae8094b58 --- /dev/null +++ b/dom/workers/test/mochitest.toml @@ -0,0 +1,365 @@ +[DEFAULT] +support-files = [ + "WorkerTest_badworker.js", + "atob_worker.js", + "blank.html", + "bug978260_worker.js", + "bug1014466_data1.txt", + "bug1014466_data2.txt", + "bug1014466_worker.js", + "bug1020226_worker.js", + "bug1020226_frame.html", + "bug998474_worker.js", + "bug1063538_worker.js", + "bug1063538.sjs", + "clearTimeouts_worker.js", + "clearTimeoutsImplicit_worker.js", + "content_worker.js", + "console_worker.js", + "consoleReplaceable_worker.js", + "csp_worker.js", + "csp_worker.js^headers^", + "dynamicImport_nested.mjs", + "dynamicImport_postMessage.mjs", + "dynamicImport_worker.js", + "404_server.sjs", + "errorPropagation_iframe.html", + "errorPropagation_worker.js", + "errorwarning_worker.js", + "eventDispatch_worker.js", + "fibonacci_worker.js", + "file_bug1010784_worker.js", + "foreign.js", + "importForeignScripts_worker.js", + "importScripts_mixedcontent.html", + "importScripts_worker.js", + "importScripts_worker_imported1.js", + "importScripts_worker_imported2.js", + "importScripts_worker_imported3.js", + "importScripts_worker_imported4.js", + "instanceof_worker.js", + "json_worker.js", + "loadEncoding_worker.js", + "location_worker.js", + "longThread_worker.js", + "multi_sharedWorker_frame.html", + "multi_sharedWorker_sharedWorker.js", + "navigator_languages_worker.js", + "navigator_worker.js", + "newError_worker.js", + "notification_worker.js", + "notification_worker_child-child.js", + "notification_worker_child-parent.js", + "notification_permission_worker.js", + "onLine_worker.js", + "onLine_worker_child.js", + "onLine_worker_head.js", + "promise_worker.js", + "recursion_worker.js", + "recursiveOnerror_worker.js", + "redirect_to_foreign.sjs", + "rvals_worker.js", + "sharedWorker_sharedWorker.js", + "simpleThread_worker.js", + "suspend_window.html", + "suspend_worker.js", + "terminate_worker.js", + "test_csp.html^headers^", + "test_csp.js", + "referrer_worker.html", + "sourcemap_header_iframe.html", + "sourcemap_header_worker.js", + "sourcemap_header_worker.js^headers^", + "threadErrors_worker1.js", + "threadErrors_worker2.js", + "threadErrors_worker3.js", + "threadErrors_worker4.js", + "threadTimeouts_worker.js", + "throwingOnerror_worker.js", + "timeoutTracing_worker.js", + "transferable_worker.js", + "test_worker_interfaces.js", + "worker_driver.js", + "worker_wrapper.js", + "bug1060621_worker.js", + "bug1062920_worker.js", + "bug1104064_worker.js", + "worker_consoleAndBlobs.js", + "bug1132395_sharedWorker.js", + "bug1132924_worker.js", + "empty.html", + "referrer.sjs", + "referrer_test_server.sjs", + "sharedWorker_ports.js", + "sharedWorker_lifetime.js", + "worker_referrer.js", + "importScripts_3rdParty_worker.js", + "invalid.js", + "worker_bug1278777.js", + "worker_setTimeoutWith0.js", + "worker_bug1301094.js", + "script_createFile.js", + "worker_suspended.js", + "window_suspended.html", + "suspend_blank.html", + "multi_sharedWorker_manager.js", + "multi_sharedWorker_frame_nobfcache.html", + "multi_sharedWorker_frame_nobfcache.html^headers^", + "multi_sharedWorker_frame_bfcache.html", + "navigate.html", + "!/dom/notification/test/mochitest/MockServices.js", + "!/dom/notification/test/mochitest/NotificationTest.js", + "!/dom/xhr/tests/relativeLoad_import.js", + "!/dom/xhr/tests/relativeLoad_worker.js", + "!/dom/xhr/tests/relativeLoad_worker2.js", + "!/dom/xhr/tests/subdir/relativeLoad_sub_worker.js", + "!/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js", + "!/dom/xhr/tests/subdir/relativeLoad_sub_import.js", + "!/dom/events/test/event_leak_utils.js", +] + +["test_404.html"] + +["test_atob.html"] + +["test_blobConstructor.html"] + +["test_blobWorkers.html"] + +["test_bug949946.html"] + +["test_bug978260.html"] + +["test_bug998474.html"] + +["test_bug1002702.html"] + +["test_bug1010784.html"] + +["test_bug1014466.html"] + +["test_bug1020226.html"] + +["test_bug1036484.html"] + +["test_bug1060621.html"] + +["test_bug1062920.html"] + +["test_bug1063538.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug1104064.html"] + +["test_bug1132395.html"] +skip-if = ["true"] # bug 1176225 + +["test_bug1132924.html"] + +["test_bug1278777.html"] + +["test_bug1301094.html"] + +["test_bug1317725.html"] +support-files = ["test_bug1317725.js"] + +["test_bug1824498.html"] +support-files = ["worker_bug1824498.mjs"] + +["test_chromeWorker.html"] + +["test_clearTimeouts.html"] + +["test_clearTimeoutsImplicit.html"] + +["test_console.html"] + +["test_consoleAndBlobs.html"] + +["test_consoleReplaceable.html"] + +["test_contentWorker.html"] + +["test_csp.html"] + +["test_dataURLWorker.html"] + +["test_dynamicImport.html"] + +["test_dynamicImport_and_terminate.html"] +support-files = ["worker_dynamicImport.mjs"] + +["test_dynamicImport_early_termination.html"] + +["test_errorPropagation.html"] +skip-if = [ + "http3", + "http2", +] + +["test_errorwarning.html"] + +["test_eventDispatch.html"] + +["test_fibonacci.html"] + +["test_fileReaderSync_when_closing.html"] + +["test_importScripts.html"] + +["test_importScripts_1.html"] + +["test_importScripts_2.html"] + +["test_importScripts_3rdparty.html"] +skip-if = [ + "http3", + "http2", +] + +["test_importScripts_mixedcontent.html"] +tags = "mcb" + +["test_instanceof.html"] + +["test_json.html"] + +["test_loadEncoding.html"] + +["test_loadError.html"] + +["test_location.html"] +skip-if = [ + "http3", + "http2", +] + +["test_longThread.html"] + +["test_multi_sharedWorker.html"] +skip-if = [ + "http3", + "http2", +] + +["test_multi_sharedWorker_lifetimes_bfcache.html"] + +["test_multi_sharedWorker_lifetimes_nobfcache.html"] + +["test_navigator.html"] +support-files = [ + "test_navigator.js", + "test_navigator_iframe.html", + "test_navigator_iframe.js", +] +skip-if = [ + "http3", + "http2", +] + +["test_navigator_languages.html"] + +["test_navigator_secureContext.html"] +scheme = "https" +support-files = [ + "test_navigator.js", + "test_navigator_iframe.html", + "test_navigator_iframe.js", +] + +["test_navigator_workers_hardwareConcurrency.html"] + +["test_newError.html"] + +["test_notification.html"] + +["test_notification_child.html"] + +["test_notification_permission.html"] + +["test_onLine.html"] + +["test_promise.html"] + +["test_promise_resolved_with_string.html"] + +["test_recursion.html"] + +["test_recursiveOnerror.html"] +skip-if = [ + "http3", + "http2", +] + +["test_referrer.html"] + +["test_referrer_header_worker.html"] +skip-if = [ + "http3", + "http2", +] + +["test_resolveWorker-assignment.html"] + +["test_resolveWorker.html"] + +["test_rvals.html"] + +["test_setTimeoutWith0.html"] + +["test_sharedWorker.html"] + +["test_sharedWorker_lifetime.html"] + +["test_sharedWorker_ports.html"] + +["test_sharedWorker_thirdparty.html"] +support-files = [ + "sharedWorker_thirdparty_frame.html", + "sharedWorker_thirdparty_window.html", +] +skip-if = [ + "http3", + "http2", +] + +["test_sharedworker_event_listener_leaks.html"] +skip-if = [ + "bits == 64 && os == 'linux' && asan", # Disabled on Linux64 opt asan, bug 1493563 + "os == 'win' && debug && xorigin", # high frequency intermittent +] + +["test_simpleThread.html"] +skip-if = [ + "http3", + "http2", +] + +["test_subworkers_suspended.html"] +scheme = "https" + +["test_suspend.html"] + +["test_terminate.html"] + +["test_threadErrors.html"] + +["test_threadTimeouts.html"] + +["test_throwingOnerror.html"] + +["test_timeoutTracing.html"] + +["test_transferable.html"] + +["test_worker_interfaces.html"] +skip-if = [ + "http3", + "http2", +] + +["test_worker_interfaces_secureContext.html"] +scheme = "https" diff --git a/dom/workers/test/multi_sharedWorker_frame.html b/dom/workers/test/multi_sharedWorker_frame.html new file mode 100644 index 0000000000..94866b918d --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_frame.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + </head> + <body> + <script type="text/javascript"> + "use strict"; + + function postMessageToParentOrOpener(message) { + if (parent != window) { + parent.postMessage(message, "*"); + } + } + + function debug(message) { + if (typeof(message) != "string") { + throw new Error("debug() only accepts strings!"); + } + postMessageToParentOrOpener(message); + } + + let worker; + + window.addEventListener("message", function(event) { + if (!worker) { + worker = new SharedWorker("multi_sharedWorker_sharedWorker.js", + "FrameWorker"); + worker.onerror = function(error) { + debug("Worker error: " + error.message); + error.preventDefault(); + + let data = { + type: "error", + message: error.message, + filename: error.filename, + lineno: error.lineno, + isErrorEvent: error instanceof ErrorEvent + }; + postMessageToParentOrOpener(data); + }; + + worker.port.onmessage = function(msg) { + debug("Worker message: " + JSON.stringify(msg.data)); + postMessageToParentOrOpener(msg.data); + }; + } + + debug("Posting message: " + JSON.stringify(event.data)); + worker.port.postMessage(event.data); + }); + </script> + </body> +</html> diff --git a/dom/workers/test/multi_sharedWorker_frame_bfcache.html b/dom/workers/test/multi_sharedWorker_frame_bfcache.html new file mode 100644 index 0000000000..dea14a8f78 --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_frame_bfcache.html @@ -0,0 +1,13 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + </head> + <body> + <script type="text/javascript" src=multi_sharedWorker_manager.js></script> + </body> +</html> diff --git a/dom/workers/test/multi_sharedWorker_frame_nobfcache.html b/dom/workers/test/multi_sharedWorker_frame_nobfcache.html new file mode 100644 index 0000000000..dea14a8f78 --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_frame_nobfcache.html @@ -0,0 +1,13 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + </head> + <body> + <script type="text/javascript" src=multi_sharedWorker_manager.js></script> + </body> +</html> diff --git a/dom/workers/test/multi_sharedWorker_frame_nobfcache.html^headers^ b/dom/workers/test/multi_sharedWorker_frame_nobfcache.html^headers^ new file mode 100644 index 0000000000..2567dc2fe5 --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_frame_nobfcache.html^headers^ @@ -0,0 +1 @@ +Cache-Control: no-store
\ No newline at end of file diff --git a/dom/workers/test/multi_sharedWorker_manager.js b/dom/workers/test/multi_sharedWorker_manager.js new file mode 100644 index 0000000000..c0a9455ef1 --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_manager.js @@ -0,0 +1,58 @@ +var query = window.location.search; +var bc = new BroadcastChannel("bugSharedWorkerLiftetime" + query); +bc.onmessage = msgEvent => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "postToWorker") { + postToWorker(msg.workerMessage); + } else if (command == "navigate") { + window.location = msg.location; + } else if (command == "finish") { + bc.postMessage({ command: "finished" }); + bc.close(); + window.close(); + } +}; + +window.onload = () => { + bc.postMessage({ command: "loaded" }); +}; + +function debug(message) { + if (typeof message != "string") { + throw new Error("debug() only accepts strings!"); + } + bc.postMessage({ command: "debug", message }); +} + +let worker; + +function postToWorker(msg) { + if (!worker) { + worker = new SharedWorker( + "multi_sharedWorker_sharedWorker.js", + "FrameWorker" + ); + worker.onerror = function (error) { + debug("Worker error: " + error.message); + error.preventDefault(); + + let data = { + type: "error", + message: error.message, + filename: error.filename, + lineno: error.lineno, + isErrorEvent: error instanceof ErrorEvent, + }; + bc.postMessage({ command: "fromWorker", workerMessage: data }); + }; + + worker.port.onmessage = function (message) { + debug("Worker message: " + JSON.stringify(message.data)); + bc.postMessage({ command: "fromWorker", workerMessage: message.data }); + }; + } + + debug("Posting message: " + JSON.stringify(msg)); + worker.port.postMessage(msg); +} diff --git a/dom/workers/test/multi_sharedWorker_sharedWorker.js b/dom/workers/test/multi_sharedWorker_sharedWorker.js new file mode 100644 index 0000000000..3c3e4c5780 --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_sharedWorker.js @@ -0,0 +1,73 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +if (self.name != "FrameWorker") { + throw new Error("Bad worker name: " + self.name); +} + +var registeredPorts = []; +var errorCount = 0; +var storedData; + +self.onconnect = function (event) { + var port = event.ports[0]; + + if (registeredPorts.length) { + let data = { + type: "connect", + }; + + registeredPorts.forEach(function (registeredPort) { + registeredPort.postMessage(data); + }); + } + + port.onmessage = function (msg) { + switch (msg.data.command) { + case "start": + break; + + case "error": + throw new Error("Expected"); + + case "store": + storedData = msg.data.data; + break; + + case "retrieve": + var data = { + type: "result", + data: storedData, + }; + port.postMessage(data); + break; + + default: + throw new Error("Unknown command '" + error.data.command + "'"); + } + }; + + registeredPorts.push(port); +}; + +self.onerror = function (message, filename, lineno) { + if (!errorCount++) { + var data = { + type: "worker-error", + message, + filename, + lineno, + }; + + registeredPorts.forEach(function (registeredPort) { + registeredPort.postMessage(data); + }); + + // Prevent the error from propagating the first time only. + return true; + } + return undefined; +}; diff --git a/dom/workers/test/navigate.html b/dom/workers/test/navigate.html new file mode 100644 index 0000000000..e38ab4540a --- /dev/null +++ b/dom/workers/test/navigate.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<script> + var bc = new BroadcastChannel("navigate"); + bc.onmessage = (event) => { + if (event.data.command == "navigate") { + window.location = event.data.location; + bc.close(); + } + } + window.onload = () => { + bc.postMessage({command: "loaded"}); + } +</script> diff --git a/dom/workers/test/navigator_languages_worker.js b/dom/workers/test/navigator_languages_worker.js new file mode 100644 index 0000000000..22ece09ef2 --- /dev/null +++ b/dom/workers/test/navigator_languages_worker.js @@ -0,0 +1,11 @@ +var active = true; +onmessage = function (e) { + if (e.data == "finish") { + active = false; + return; + } + + if (active) { + postMessage(navigator.languages); + } +}; diff --git a/dom/workers/test/navigator_worker.js b/dom/workers/test/navigator_worker.js new file mode 100644 index 0000000000..a9cd4f29d5 --- /dev/null +++ b/dom/workers/test/navigator_worker.js @@ -0,0 +1,87 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// IMPORTANT: Do not change the list below without review from a DOM peer! +var supportedProps = [ + "appCodeName", + "appName", + "appVersion", + "globalPrivacyControl", + "platform", + "product", + "userAgent", + "onLine", + "language", + "languages", + { name: "locks", isSecureContext: true }, + "mediaCapabilities", + "hardwareConcurrency", + { name: "storage", isSecureContext: true }, + "connection", +]; + +self.onmessage = function (event) { + if (!event || !event.data) { + return; + } + + startTest(event.data); +}; + +function startTest(channelData) { + // Prepare the interface map showing if a propery should exist in this build. + // For example, if interfaceMap[foo] = true means navigator.foo should exist. + var interfaceMap = {}; + + for (var prop of supportedProps) { + if (typeof prop === "string") { + interfaceMap[prop] = true; + continue; + } + + if ( + prop.isNightly === !channelData.isNightly || + prop.release === !channelData.isRelease || + prop.isSecureContext === !isSecureContext || + prop.isAndroid === !channelData.isAndroid + ) { + interfaceMap[prop.name] = false; + continue; + } + + interfaceMap[prop.name] = true; + } + + for (var prop in navigator) { + // Make sure the list is current! + if (!interfaceMap[prop]) { + throw "Navigator has the '" + prop + "' property that isn't in the list!"; + } + } + + var obj; + + for (var prop in interfaceMap) { + // Skip the property that is not supposed to exist in this build. + if (!interfaceMap[prop]) { + continue; + } + + if (typeof navigator[prop] == "undefined") { + throw "Navigator has no '" + prop + "' property!"; + } + + obj = { name: prop }; + obj.value = navigator[prop]; + + postMessage(JSON.stringify(obj)); + } + + obj = { + name: "testFinished", + }; + + postMessage(JSON.stringify(obj)); +} diff --git a/dom/workers/test/newError_worker.js b/dom/workers/test/newError_worker.js new file mode 100644 index 0000000000..46e6226f73 --- /dev/null +++ b/dom/workers/test/newError_worker.js @@ -0,0 +1,5 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +throw new Error("foo!"); diff --git a/dom/workers/test/notification_permission_worker.js b/dom/workers/test/notification_permission_worker.js new file mode 100644 index 0000000000..0e6b96d975 --- /dev/null +++ b/dom/workers/test/notification_permission_worker.js @@ -0,0 +1,59 @@ +function info(message) { + dump("INFO: " + message + "\n"); +} + +function ok(test, message) { + postMessage({ type: "ok", test, message }); +} + +function is(a, b, message) { + postMessage({ type: "is", test1: a, test2: b, message }); +} + +if (self.Notification) { + var steps = [ + function (done) { + info("Test notification permission"); + ok(typeof Notification === "function", "Notification constructor exists"); + ok( + Notification.permission === "denied", + "Notification.permission is denied." + ); + + var n = new Notification("Hello"); + n.onerror = function (e) { + ok(true, "error called due to permission denied."); + done(); + }; + }, + ]; + + onmessage = function (e) { + var context = {}; + (function executeRemainingTests(remainingTests) { + if (!remainingTests.length) { + postMessage({ type: "finish" }); + return; + } + + var nextTest = remainingTests.shift(); + var finishTest = executeRemainingTests.bind(null, remainingTests); + var startTest = nextTest.call.bind(nextTest, context, finishTest); + + try { + startTest(); + // if no callback was defined for test function, + // we must manually invoke finish to continue + if (nextTest.length === 0) { + finishTest(); + } + } catch (ex) { + ok(false, "Test threw exception! " + nextTest + " " + ex); + finishTest(); + } + })(steps); + }; +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: "finish" }); +} diff --git a/dom/workers/test/notification_worker.js b/dom/workers/test/notification_worker.js new file mode 100644 index 0000000000..87aa02ac05 --- /dev/null +++ b/dom/workers/test/notification_worker.js @@ -0,0 +1,104 @@ +function ok(test, message) { + postMessage({ type: "ok", test, message }); +} + +function is(a, b, message) { + postMessage({ type: "is", test1: a, test2: b, message }); +} + +if (self.Notification) { + var steps = [ + function () { + ok(typeof Notification === "function", "Notification constructor exists"); + ok(Notification.permission, "Notification.permission exists"); + ok( + typeof Notification.requestPermission === "undefined", + "Notification.requestPermission should not exist" + ); + }, + + function (done) { + var options = { + dir: "auto", + lang: "", + body: "This is a notification body", + tag: "sometag", + icon: "icon.png", + data: ["a complex object that should be", { structured: "cloned" }], + mozbehavior: { vibrationPattern: [30, 200, 30] }, + }; + var notification = new Notification("This is a title", options); + + ok(notification !== undefined, "Notification exists"); + is(notification.onclick, null, "onclick() should be null"); + is(notification.onshow, null, "onshow() should be null"); + is(notification.onerror, null, "onerror() should be null"); + is(notification.onclose, null, "onclose() should be null"); + is(typeof notification.close, "function", "close() should exist"); + + is(notification.dir, options.dir, "auto should get set"); + is(notification.lang, options.lang, "lang should get set"); + is(notification.body, options.body, "body should get set"); + is(notification.tag, options.tag, "tag should get set"); + is(notification.icon, options.icon, "icon should get set"); + is( + notification.data[0], + "a complex object that should be", + "data item 0 should be a matching string" + ); + is( + notification.data[1].structured, + "cloned", + "data item 1 should be a matching object literal" + ); + + // store notification in test context + this.notification = notification; + + notification.onshow = function () { + ok(true, "onshow handler should be called"); + done(); + }; + }, + + function (done) { + var notification = this.notification; + + notification.onclose = function () { + ok(true, "onclose handler should be called"); + done(); + }; + + notification.close(); + }, + ]; + + onmessage = function (e) { + var context = {}; + (function executeRemainingTests(remainingTests) { + if (!remainingTests.length) { + postMessage({ type: "finish" }); + return; + } + + var nextTest = remainingTests.shift(); + var finishTest = executeRemainingTests.bind(null, remainingTests); + var startTest = nextTest.call.bind(nextTest, context, finishTest); + + try { + startTest(); + // if no callback was defined for test function, + // we must manually invoke finish to continue + if (nextTest.length === 0) { + finishTest(); + } + } catch (ex) { + ok(false, "Test threw exception! " + nextTest + " " + ex); + finishTest(); + } + })(steps); + }; +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: "finish" }); +} diff --git a/dom/workers/test/notification_worker_child-child.js b/dom/workers/test/notification_worker_child-child.js new file mode 100644 index 0000000000..236e314e47 --- /dev/null +++ b/dom/workers/test/notification_worker_child-child.js @@ -0,0 +1,93 @@ +function ok(test, message) { + postMessage({ type: "ok", test, message }); +} + +function is(a, b, message) { + postMessage({ type: "is", test1: a, test2: b, message }); +} + +if (self.Notification) { + var steps = [ + function () { + ok(typeof Notification === "function", "Notification constructor exists"); + ok(Notification.permission, "Notification.permission exists"); + ok( + typeof Notification.requestPermission === "undefined", + "Notification.requestPermission should not exist" + ); + //ok(typeof Notification.get === "function", "Notification.get exists"); + }, + + function (done) { + var options = { + dir: "auto", + lang: "", + body: "This is a notification body", + tag: "sometag", + icon: "icon.png", + }; + var notification = new Notification("This is a title", options); + + ok(notification !== undefined, "Notification exists"); + is(notification.onclick, null, "onclick() should be null"); + is(notification.onshow, null, "onshow() should be null"); + is(notification.onerror, null, "onerror() should be null"); + is(notification.onclose, null, "onclose() should be null"); + is(typeof notification.close, "function", "close() should exist"); + + is(notification.dir, options.dir, "auto should get set"); + is(notification.lang, options.lang, "lang should get set"); + is(notification.body, options.body, "body should get set"); + is(notification.tag, options.tag, "tag should get set"); + is(notification.icon, options.icon, "icon should get set"); + + // store notification in test context + this.notification = notification; + + notification.onshow = function () { + ok(true, "onshow handler should be called"); + done(); + }; + }, + + function (done) { + var notification = this.notification; + + notification.onclose = function () { + ok(true, "onclose handler should be called"); + done(); + }; + + notification.close(); + }, + ]; + + onmessage = function (e) { + var context = {}; + (function executeRemainingTests(remainingTests) { + if (!remainingTests.length) { + postMessage({ type: "finish" }); + return; + } + + var nextTest = remainingTests.shift(); + var finishTest = executeRemainingTests.bind(null, remainingTests); + var startTest = nextTest.call.bind(nextTest, context, finishTest); + + try { + startTest(); + // if no callback was defined for test function, + // we must manually invoke finish to continue + if (nextTest.length === 0) { + finishTest(); + } + } catch (ex) { + ok(false, "Test threw exception! " + nextTest + " " + ex); + finishTest(); + } + })(steps); + }; +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: "finish" }); +} diff --git a/dom/workers/test/notification_worker_child-parent.js b/dom/workers/test/notification_worker_child-parent.js new file mode 100644 index 0000000000..45dd061993 --- /dev/null +++ b/dom/workers/test/notification_worker_child-parent.js @@ -0,0 +1,26 @@ +function ok(test, message) { + postMessage({ type: "ok", test, message }); +} + +function is(a, b, message) { + postMessage({ type: "is", test1: a, test2: b, message }); +} + +if (self.Notification) { + var child = new Worker("notification_worker_child-child.js"); + child.onerror = function (e) { + ok(false, "Error loading child worker " + e); + postMessage({ type: "finish" }); + }; + + child.onmessage = function (e) { + postMessage(e.data); + }; + + onmessage = function (e) { + child.postMessage("start"); + }; +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: "finish" }); +} diff --git a/dom/workers/test/onLine_worker.js b/dom/workers/test/onLine_worker.js new file mode 100644 index 0000000000..94c10c699c --- /dev/null +++ b/dom/workers/test/onLine_worker.js @@ -0,0 +1,70 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +importScripts("onLine_worker_head.js"); + +var N_CHILDREN = 3; +var children = []; +var finishedChildrenCount = 0; +var lastTest = false; + +for (var event of ["online", "offline"]) { + addEventListener( + event, + makeHandler( + "addEventListener('%1', ..., false)", + event, + 1, + "Parent Worker" + ), + false + ); +} + +onmessage = function (e) { + if (e.data === "lastTest") { + children.forEach(function (w) { + w.postMessage({ type: "lastTest" }); + }); + lastTest = true; + } +}; + +function setupChildren(cb) { + var readyCount = 0; + for (var i = 0; i < N_CHILDREN; ++i) { + var w = new Worker("onLine_worker_child.js"); + children.push(w); + + w.onerror = function (e) { + info("Error creating child " + e.message); + }; + + w.onmessage = function (e) { + if (e.data.type === "ready") { + info("Got ready from child"); + readyCount++; + if (readyCount === N_CHILDREN) { + cb(); + } + } else if (e.data.type === "finished") { + finishedChildrenCount++; + + if (lastTest && finishedChildrenCount === N_CHILDREN) { + postMessage({ type: "finished" }); + children = []; + close(); + } + } else if (e.data.type === "ok") { + // Pass on test to page. + postMessage(e.data); + } + }; + } +} + +setupChildren(function () { + postMessage({ type: "ready" }); +}); diff --git a/dom/workers/test/onLine_worker_child.js b/dom/workers/test/onLine_worker_child.js new file mode 100644 index 0000000000..92542c018f --- /dev/null +++ b/dom/workers/test/onLine_worker_child.js @@ -0,0 +1,91 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +function info(text) { + dump("Test for Bug 925437: worker: " + text + "\n"); +} + +function ok(test, message) { + postMessage({ type: "ok", test, message }); +} + +/** + * Returns a handler function for an online/offline event. The returned handler + * ensures the passed event object has expected properties and that the handler + * is called at the right moment (according to the gState variable). + * @param nameTemplate The string identifying the hanlder. '%1' in that + * string will be replaced with the event name. + * @param eventName 'online' or 'offline' + * @param expectedState value of gState at the moment the handler is called. + * The handler increases gState by one before checking + * if it matches expectedState. + */ +function makeHandler(nameTemplate, eventName, expectedState, prefix, custom) { + prefix += ": "; + return function (e) { + var name = nameTemplate.replace(/%1/, eventName); + ok(e.constructor == Event, prefix + "event should be an Event"); + ok(e.type == eventName, prefix + "event type should be " + eventName); + ok(!e.bubbles, prefix + "event should not bubble"); + ok(!e.cancelable, prefix + "event should not be cancelable"); + ok( + e.target == self, + prefix + "the event target should be the worker scope" + ); + ok( + eventName == "online" ? navigator.onLine : !navigator.onLine, + prefix + + "navigator.onLine " + + navigator.onLine + + " should reflect event " + + eventName + ); + + if (custom) { + custom(); + } + }; +} + +var lastTest = false; + +function lastTestTest() { + if (lastTest) { + postMessage({ type: "finished" }); + close(); + } +} + +for (var event of ["online", "offline"]) { + addEventListener( + event, + makeHandler( + "addEventListener('%1', ..., false)", + event, + 1, + "Child Worker", + lastTestTest + ), + false + ); +} + +onmessage = function (e) { + if (e.data.type === "lastTest") { + lastTest = true; + } else if (e.data.type === "navigatorState") { + ok( + e.data.state === navigator.onLine, + "Child and parent navigator state should match" + ); + } +}; + +postMessage({ type: "ready" }); diff --git a/dom/workers/test/onLine_worker_head.js b/dom/workers/test/onLine_worker_head.js new file mode 100644 index 0000000000..632821b1f4 --- /dev/null +++ b/dom/workers/test/onLine_worker_head.js @@ -0,0 +1,50 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +function info(text) { + dump("Test for Bug 925437: worker: " + text + "\n"); +} + +function ok(test, message) { + postMessage({ type: "ok", test, message }); +} + +/** + * Returns a handler function for an online/offline event. The returned handler + * ensures the passed event object has expected properties and that the handler + * is called at the right moment (according to the gState variable). + * @param nameTemplate The string identifying the hanlder. '%1' in that + * string will be replaced with the event name. + * @param eventName 'online' or 'offline' + * @param expectedState value of gState at the moment the handler is called. + * The handler increases gState by one before checking + * if it matches expectedState. + */ +function makeHandler(nameTemplate, eventName, expectedState, prefix, custom) { + prefix += ": "; + return function (e) { + var name = nameTemplate.replace(/%1/, eventName); + ok(e.constructor == Event, prefix + "event should be an Event"); + ok(e.type == eventName, prefix + "event type should be " + eventName); + ok(!e.bubbles, prefix + "event should not bubble"); + ok(!e.cancelable, prefix + "event should not be cancelable"); + ok( + e.target == self, + prefix + "the event target should be the worker scope" + ); + ok( + eventName == "online" ? navigator.onLine : !navigator.onLine, + prefix + + "navigator.onLine " + + navigator.onLine + + " should reflect event " + + eventName + ); + + if (custom) { + custom(); + } + }; +} diff --git a/dom/workers/test/promise_worker.js b/dom/workers/test/promise_worker.js new file mode 100644 index 0000000000..fd4a9177d6 --- /dev/null +++ b/dom/workers/test/promise_worker.js @@ -0,0 +1,1007 @@ +"use strict"; + +function ok(a, msg) { + dump("OK: " + !!a + " => " + a + " " + msg + "\n"); + postMessage({ type: "status", status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + dump("IS: " + (a === b) + " => " + a + " | " + b + " " + msg + "\n"); + postMessage({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + }); +} + +function isnot(a, b, msg) { + dump("ISNOT: " + (a !== b) + " => " + a + " | " + b + " " + msg + "\n"); + postMessage({ + type: "status", + status: a !== b, + msg: a + " !== " + b + ": " + msg, + }); +} + +function promiseResolve() { + ok(Promise, "Promise object should exist"); + + var promise = new Promise(function (resolve, reject) { + ok(resolve, "Promise.resolve exists"); + ok(reject, "Promise.reject exists"); + + resolve(42); + }).then( + function (what) { + ok(true, "Then - resolveCb has been called"); + is(what, 42, "ResolveCb received 42"); + runTest(); + }, + function () { + ok(false, "Then - rejectCb has been called"); + runTest(); + } + ); +} + +function promiseResolveNoArg() { + var promise = new Promise(function (resolve, reject) { + ok(resolve, "Promise.resolve exists"); + ok(reject, "Promise.reject exists"); + + resolve(); + }).then( + function (what) { + ok(true, "Then - resolveCb has been called"); + is(what, undefined, "ResolveCb received undefined"); + runTest(); + }, + function () { + ok(false, "Then - rejectCb has been called"); + runTest(); + } + ); +} + +function promiseRejectNoHandler() { + // This test only checks that the code that reports unhandled errors in the + // Promises implementation does not crash or leak. + var promise = new Promise(function (res, rej) { + noSuchMethod(); + }); + runTest(); +} + +function promiseReject() { + var promise = new Promise(function (resolve, reject) { + reject(42); + }).then( + function (what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, + function (what) { + ok(true, "Then - rejectCb has been called"); + is(what, 42, "RejectCb received 42"); + runTest(); + } + ); +} + +function promiseRejectNoArg() { + var promise = new Promise(function (resolve, reject) { + reject(); + }).then( + function (what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, + function (what) { + ok(true, "Then - rejectCb has been called"); + is(what, undefined, "RejectCb received undefined"); + runTest(); + } + ); +} + +function promiseException() { + var promise = new Promise(function (resolve, reject) { + throw 42; + }).then( + function (what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, + function (what) { + ok(true, "Then - rejectCb has been called"); + is(what, 42, "RejectCb received 42"); + runTest(); + } + ); +} + +function promiseAsync_TimeoutResolveThen() { + var handlerExecuted = false; + + setTimeout(function () { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + Promise.resolve().then(function () { + handlerExecuted = true; + }); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_ResolveTimeoutThen() { + var handlerExecuted = false; + + var promise = Promise.resolve(); + + setTimeout(function () { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + promise.then(function () { + handlerExecuted = true; + }); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_ResolveThenTimeout() { + var handlerExecuted = false; + + Promise.resolve().then(function () { + handlerExecuted = true; + }); + + setTimeout(function () { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_SyncXHRAndImportScripts() { + var handlerExecuted = false; + + Promise.resolve().then(function () { + handlerExecuted = true; + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }); + + ok(!handlerExecuted, "Handlers are not called until the next microtask."); + + var xhr = new XMLHttpRequest(); + xhr.open("GET", "testXHR.txt", false); + xhr.send(null); + + ok(!handlerExecuted, "Sync XHR should not trigger microtask execution."); + + importScripts("../../../dom/xhr/tests/relativeLoad_import.js"); + + ok(!handlerExecuted, "importScripts should not trigger microtask execution."); +} + +function promiseDoubleThen() { + var steps = 0; + var promise = new Promise(function (r1, r2) { + r1(42); + }); + + promise.then( + function (what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + steps++; + }, + function (what) { + ok(false, "Then.reject has been called"); + } + ); + + promise.then( + function (what) { + ok(true, "Then.resolve has been called"); + is(steps, 1, "Then.resolve - step == 1"); + is(what, 42, "Value == 42"); + runTest(); + }, + function (what) { + ok(false, "Then.reject has been called"); + } + ); +} + +function promiseThenException() { + var promise = new Promise(function (resolve, reject) { + resolve(42); + }); + + promise + .then(function (what) { + ok(true, "Then.resolve has been called"); + throw "booh"; + }) + .catch(function (e) { + ok(true, "Catch has been called!"); + runTest(); + }); +} + +function promiseThenCatchThen() { + var promise = new Promise(function (resolve, reject) { + resolve(42); + }); + + var promise2 = promise.then( + function (what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + return what + 1; + }, + function (what) { + ok(false, "Then.reject has been called"); + } + ); + + isnot(promise, promise2, "These 2 promise objs are different"); + + promise2 + .then( + function (what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + return what + 1; + }, + function (what) { + ok(false, "Then.reject has been called"); + } + ) + .catch(function () { + ok(false, "Catch has been called"); + }) + .then( + function (what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }, + function (what) { + ok(false, "Then.reject has been called"); + } + ); +} + +function promiseRejectThenCatchThen() { + var promise = new Promise(function (resolve, reject) { + reject(42); + }); + + var promise2 = promise.then( + function (what) { + ok(false, "Then.resolve has been called"); + }, + function (what) { + ok(true, "Then.reject has been called"); + is(what, 42, "Value == 42"); + return what + 1; + } + ); + + isnot(promise, promise2, "These 2 promise objs are different"); + + promise2 + .then(function (what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + return what + 1; + }) + .catch(function (what) { + ok(false, "Catch has been called"); + }) + .then(function (what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }); +} + +function promiseRejectThenCatchThen2() { + var promise = new Promise(function (resolve, reject) { + reject(42); + }); + + promise + .then(function (what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + return what + 1; + }) + .catch(function (what) { + is(what, 42, "Value == 42"); + ok(true, "Catch has been called"); + return what + 1; + }) + .then(function (what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + runTest(); + }); +} + +function promiseRejectThenCatchExceptionThen() { + var promise = new Promise(function (resolve, reject) { + reject(42); + }); + + promise + .then( + function (what) { + ok(false, "Then.resolve has been called"); + }, + function (what) { + ok(true, "Then.reject has been called"); + is(what, 42, "Value == 42"); + throw what + 1; + } + ) + .catch(function (what) { + ok(true, "Catch has been called"); + is(what, 43, "Value == 43"); + return what + 1; + }) + .then(function (what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }); +} + +function promiseThenCatchOrderingResolve() { + var global = 0; + var f = new Promise(function (r1, r2) { + r1(42); + }); + + f.then(function () { + f.then(function () { + global++; + }); + f.catch(function () { + global++; + }); + f.then(function () { + global++; + }); + setTimeout(function () { + is(global, 2, "Many steps... should return 2"); + runTest(); + }, 0); + }); +} + +function promiseThenCatchOrderingReject() { + var global = 0; + var f = new Promise(function (r1, r2) { + r2(42); + }); + + f.then( + function () {}, + function () { + f.then(function () { + global++; + }); + f.catch(function () { + global++; + }); + f.then( + function () {}, + function () { + global++; + } + ); + setTimeout(function () { + is(global, 2, "Many steps... should return 2"); + runTest(); + }, 0); + } + ); +} + +function promiseThenNoArg() { + var promise = new Promise(function (resolve, reject) { + resolve(42); + }); + + var clone = promise.then(); + isnot(promise, clone, "These 2 promise objs are different"); + promise.then(function (v) { + clone.then(function (cv) { + is(v, cv, "Both resolve to the same value"); + runTest(); + }); + }); +} + +function promiseThenUndefinedResolveFunction() { + var promise = new Promise(function (resolve, reject) { + reject(42); + }); + + try { + promise.then(undefined, function (v) { + is(v, 42, "Promise rejected with 42"); + runTest(); + }); + } catch (e) { + ok(false, "then should not throw on undefined resolve function"); + } +} + +function promiseThenNullResolveFunction() { + var promise = new Promise(function (resolve, reject) { + reject(42); + }); + + try { + promise.then(null, function (v) { + is(v, 42, "Promise rejected with 42"); + runTest(); + }); + } catch (e) { + ok(false, "then should not throw on null resolve function"); + } +} + +function promiseCatchNoArg() { + var promise = new Promise(function (resolve, reject) { + reject(42); + }); + + var clone = promise.catch(); + isnot(promise, clone, "These 2 promise objs are different"); + promise.catch(function (v) { + clone.catch(function (cv) { + is(v, cv, "Both reject to the same value"); + runTest(); + }); + }); +} + +function promiseNestedPromise() { + new Promise(function (resolve, reject) { + resolve( + new Promise(function (r) { + ok(true, "Nested promise is executed"); + r(42); + }) + ); + }).then(function (value) { + is(value, 42, "Nested promise is executed and then == 42"); + runTest(); + }); +} + +function promiseNestedNestedPromise() { + new Promise(function (resolve, reject) { + resolve( + new Promise(function (r) { + ok(true, "Nested promise is executed"); + r(42); + }).then(function (what) { + return what + 1; + }) + ); + }).then(function (value) { + is(value, 43, "Nested promise is executed and then == 43"); + runTest(); + }); +} + +function promiseWrongNestedPromise() { + new Promise(function (resolve, reject) { + resolve( + new Promise(function (r, r2) { + ok(true, "Nested promise is executed"); + r(42); + }) + ); + reject(42); + }).then( + function (value) { + is(value, 42, "Nested promise is executed and then == 42"); + runTest(); + }, + function (value) { + ok(false, "This is wrong"); + } + ); +} + +function promiseLoop() { + new Promise(function (resolve, reject) { + resolve( + new Promise(function (r1, r2) { + ok(true, "Nested promise is executed"); + r1( + new Promise(function (r3, r4) { + ok(true, "Nested nested promise is executed"); + r3(42); + }) + ); + }) + ); + }).then( + function (value) { + is(value, 42, "Nested nested promise is executed and then == 42"); + runTest(); + }, + function (value) { + ok(false, "This is wrong"); + } + ); +} + +function promiseStaticReject() { + var promise = Promise.reject(42).then( + function (what) { + ok(false, "This should not be called"); + }, + function (what) { + is(what, 42, "Value == 42"); + runTest(); + } + ); +} + +function promiseStaticResolve() { + var promise = Promise.resolve(42).then( + function (what) { + is(what, 42, "Value == 42"); + runTest(); + }, + function () { + ok(false, "This should not be called"); + } + ); +} + +function promiseResolveNestedPromise() { + var promise = Promise.resolve( + new Promise( + function (r, r2) { + ok(true, "Nested promise is executed"); + r(42); + }, + function () { + ok(false, "This should not be called"); + } + ) + ).then( + function (what) { + is(what, 42, "Value == 42"); + runTest(); + }, + function () { + ok(false, "This should not be called"); + } + ); +} + +function promiseUtilitiesDefined() { + ok(Promise.all, "Promise.all must be defined when Promise is enabled."); + ok(Promise.race, "Promise.race must be defined when Promise is enabled."); + runTest(); +} + +function promiseAllArray() { + var p = Promise.all([1, new Date(), Promise.resolve("firefox")]); + ok(p instanceof Promise, "Return value of Promise.all should be a Promise."); + p.then( + function (values) { + ok(Array.isArray(values), "Resolved value should be an array."); + is( + values.length, + 3, + "Resolved array length should match iterable's length." + ); + is(values[0], 1, "Array values should match."); + ok(values[1] instanceof Date, "Array values should match."); + is(values[2], "firefox", "Array values should match."); + runTest(); + }, + function () { + ok( + false, + "Promise.all shouldn't fail when iterable has no rejected Promises." + ); + runTest(); + } + ); +} + +function promiseAllWaitsForAllPromises() { + var arr = [ + new Promise(function (resolve) { + setTimeout(resolve.bind(undefined, 1), 50); + }), + new Promise(function (resolve) { + setTimeout(resolve.bind(undefined, 2), 10); + }), + new Promise(function (resolve) { + setTimeout( + resolve.bind( + undefined, + new Promise(function (resolve2) { + resolve2(3); + }) + ), + 10 + ); + }), + new Promise(function (resolve) { + setTimeout(resolve.bind(undefined, 4), 20); + }), + ]; + + var p = Promise.all(arr); + p.then( + function (values) { + ok(Array.isArray(values), "Resolved value should be an array."); + is( + values.length, + 4, + "Resolved array length should match iterable's length." + ); + is(values[0], 1, "Array values should match."); + is(values[1], 2, "Array values should match."); + is(values[2], 3, "Array values should match."); + is(values[3], 4, "Array values should match."); + runTest(); + }, + function () { + ok( + false, + "Promise.all shouldn't fail when iterable has no rejected Promises." + ); + runTest(); + } + ); +} + +function promiseAllRejectFails() { + var arr = [ + new Promise(function (resolve) { + setTimeout(resolve.bind(undefined, 1), 50); + }), + new Promise(function (resolve, reject) { + setTimeout(reject.bind(undefined, 2), 10); + }), + new Promise(function (resolve) { + setTimeout(resolve.bind(undefined, 3), 10); + }), + new Promise(function (resolve) { + setTimeout(resolve.bind(undefined, 4), 20); + }), + ]; + + var p = Promise.all(arr); + p.then( + function (values) { + ok( + false, + "Promise.all shouldn't resolve when iterable has rejected Promises." + ); + runTest(); + }, + function (e) { + ok( + true, + "Promise.all should reject when iterable has rejected Promises." + ); + is(e, 2, "Rejection value should match."); + runTest(); + } + ); +} + +function promiseRaceEmpty() { + var p = Promise.race([]); + ok(p instanceof Promise, "Should return a Promise."); + // An empty race never resolves! + runTest(); +} + +function promiseRaceValuesArray() { + var p = Promise.race([true, new Date(), 3]); + ok(p instanceof Promise, "Should return a Promise."); + p.then( + function (winner) { + is(winner, true, "First value should win."); + runTest(); + }, + function (err) { + ok(false, "Should not fail " + err + "."); + runTest(); + } + ); +} + +function promiseRacePromiseArray() { + var arr = [ + new Promise(function (resolve) { + resolve("first"); + }), + Promise.resolve("second"), + new Promise(function () {}), + new Promise(function (resolve) { + setTimeout(function () { + setTimeout(function () { + resolve("fourth"); + }, 0); + }, 0); + }), + ]; + + var p = Promise.race(arr); + p.then(function (winner) { + is(winner, "first", "First queued resolution should win the race."); + runTest(); + }); +} + +function promiseRaceReject() { + var p = Promise.race([ + Promise.reject(new Error("Fail bad!")), + new Promise(function (resolve) { + setTimeout(resolve, 0); + }), + ]); + + p.then( + function () { + ok(false, "Should not resolve when winning Promise rejected."); + runTest(); + }, + function (e) { + ok(true, "Should be rejected"); + ok(e instanceof Error, "Should reject with Error."); + ok(e.message == "Fail bad!", "Message should match."); + runTest(); + } + ); +} + +function promiseRaceThrow() { + var p = Promise.race([ + new Promise(function (resolve) { + nonExistent(); + }), + new Promise(function (resolve) { + setTimeout(resolve, 0); + }), + ]); + + p.then( + function () { + ok(false, "Should not resolve when winning Promise had an error."); + runTest(); + }, + function (e) { + ok(true, "Should be rejected"); + ok( + e instanceof ReferenceError, + "Should reject with ReferenceError for function nonExistent()." + ); + runTest(); + } + ); +} + +function promiseResolveArray() { + var p = Promise.resolve([1, 2, 3]); + ok(p instanceof Promise, "Should return a Promise."); + p.then(function (v) { + ok(Array.isArray(v), "Resolved value should be an Array"); + is(v.length, 3, "Length should match"); + is(v[0], 1, "Resolved value should match original"); + is(v[1], 2, "Resolved value should match original"); + is(v[2], 3, "Resolved value should match original"); + runTest(); + }); +} + +function promiseResolveThenable() { + var p = Promise.resolve({ + then(onFulfill, onReject) { + onFulfill(2); + }, + }); + ok(p instanceof Promise, "Should cast to a Promise."); + p.then( + function (v) { + is(v, 2, "Should resolve to 2."); + runTest(); + }, + function (e) { + ok(false, "promiseResolveThenable should've resolved"); + runTest(); + } + ); +} + +function promiseResolvePromise() { + var original = Promise.resolve(true); + var cast = Promise.resolve(original); + + ok(cast instanceof Promise, "Should cast to a Promise."); + is(cast, original, "Should return original Promise."); + cast.then(function (v) { + is(v, true, "Should resolve to true."); + runTest(); + }); +} + +// Bug 1009569. +// Ensure that thenables are run on a clean stack asynchronously. +// Test case adopted from +// https://gist.github.com/getify/d64bb01751b50ed6b281#file-bug1-js. +function promiseResolveThenableCleanStack() { + function immed(s) { + x++; + s(); + } + function incX() { + x++; + } + + var x = 0; + var thenable = { then: immed }; + var results = []; + + var p = Promise.resolve(thenable).then(incX); + results.push(x); + + // check what happens after all "next cycle" steps + // have had a chance to complete + setTimeout(function () { + // Result should be [0, 2] since `thenable` will be called async. + is(results[0], 0, "Expected thenable to be called asynchronously"); + // See Bug 1023547 comment 13 for why this check has to be gated on p. + p.then(function () { + results.push(x); + is(results[1], 2, "Expected thenable to be called asynchronously"); + runTest(); + }); + }, 1000); +} + +// Bug 1062323 +function promiseWrapperAsyncResolution() { + var p = new Promise(function (resolve, reject) { + resolve(); + }); + + var results = []; + var q = p + .then(function () { + results.push("1-1"); + }) + .then(function () { + results.push("1-2"); + }) + .then(function () { + results.push("1-3"); + }); + + var r = p + .then(function () { + results.push("2-1"); + }) + .then(function () { + results.push("2-2"); + }) + .then(function () { + results.push("2-3"); + }); + + Promise.all([q, r]).then( + function () { + var match = + results[0] == "1-1" && + results[1] == "2-1" && + results[2] == "1-2" && + results[3] == "2-2" && + results[4] == "1-3" && + results[5] == "2-3"; + ok(match, "Chained promises should resolve asynchronously."); + runTest(); + }, + function () { + ok(false, "promiseWrapperAsyncResolution: One of the promises failed."); + runTest(); + } + ); +} + +var tests = [ + promiseResolve, + promiseReject, + promiseException, + promiseAsync_TimeoutResolveThen, + promiseAsync_ResolveTimeoutThen, + promiseAsync_ResolveThenTimeout, + promiseAsync_SyncXHRAndImportScripts, + promiseDoubleThen, + promiseThenException, + promiseThenCatchThen, + promiseRejectThenCatchThen, + promiseRejectThenCatchThen2, + promiseRejectThenCatchExceptionThen, + promiseThenCatchOrderingResolve, + promiseThenCatchOrderingReject, + promiseNestedPromise, + promiseNestedNestedPromise, + promiseWrongNestedPromise, + promiseLoop, + promiseStaticReject, + promiseStaticResolve, + promiseResolveNestedPromise, + promiseResolveNoArg, + promiseRejectNoArg, + + promiseThenNoArg, + promiseThenUndefinedResolveFunction, + promiseThenNullResolveFunction, + promiseCatchNoArg, + promiseRejectNoHandler, + + promiseUtilitiesDefined, + + promiseAllArray, + promiseAllWaitsForAllPromises, + promiseAllRejectFails, + + promiseRaceEmpty, + promiseRaceValuesArray, + promiseRacePromiseArray, + promiseRaceReject, + promiseRaceThrow, + + promiseResolveArray, + promiseResolveThenable, + promiseResolvePromise, + + promiseResolveThenableCleanStack, + + promiseWrapperAsyncResolution, +]; + +function runTest() { + if (!tests.length) { + postMessage({ type: "finish" }); + return; + } + + var test = tests.shift(); + test(); +} + +onmessage = function () { + runTest(); +}; diff --git a/dom/workers/test/recursion_worker.js b/dom/workers/test/recursion_worker.js new file mode 100644 index 0000000000..84eb6d9740 --- /dev/null +++ b/dom/workers/test/recursion_worker.js @@ -0,0 +1,47 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This function should never run on a too much recursion error. +onerror = function (event) { + postMessage(event.message); +}; + +// Pure JS recursion +function recurse() { + recurse(); +} + +// JS -> C++ -> JS -> C++ recursion +function recurse2() { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + xhr.open("GET", "nonexistent.file"); + }; + xhr.open("GET", "nonexistent.file"); +} + +var messageCount = 0; +onmessage = function (event) { + switch (++messageCount) { + case 2: + recurse2(); + + // An exception thrown from an event handler like xhr.onreadystatechange + // should not leave an exception pending in the code that generated the + // event. + postMessage("Done"); + return; + + case 1: + recurse(); + throw "Exception should have prevented us from getting here!"; + + default: + throw "Weird number of messages: " + messageCount; + } + + // eslint-disable-next-line no-unreachable + throw "Impossible to get here!"; +}; diff --git a/dom/workers/test/recursiveOnerror_worker.js b/dom/workers/test/recursiveOnerror_worker.js new file mode 100644 index 0000000000..35d2e2b80d --- /dev/null +++ b/dom/workers/test/recursiveOnerror_worker.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onerror = function (message, filename, lineno) { + throw new Error("2"); +}; + +onmessage = function (event) { + throw new Error("1"); +}; diff --git a/dom/workers/test/redirect_to_foreign.sjs b/dom/workers/test/redirect_to_foreign.sjs new file mode 100644 index 0000000000..06fd12052b --- /dev/null +++ b/dom/workers/test/redirect_to_foreign.sjs @@ -0,0 +1,7 @@ +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "http://example.org/tests/dom/workers/test/foreign.js" + ); +} diff --git a/dom/workers/test/referrer.sjs b/dom/workers/test/referrer.sjs new file mode 100644 index 0000000000..f91c434c16 --- /dev/null +++ b/dom/workers/test/referrer.sjs @@ -0,0 +1,14 @@ +function handleRequest(request, response) { + if (request.queryString == "result") { + response.write(getState("referer")); + setState("referer", "INVALID"); + } else if (request.queryString == "worker") { + response.setHeader("Content-Type", "text/javascript", false); + response.write("onmessage = function() { postMessage(42); }"); + setState("referer", request.getHeader("referer")); + } else if (request.queryString == "import") { + setState("referer", request.getHeader("referer")); + response.setHeader("Content-Type", "text/javascript", false); + response.write("'hello world'"); + } +} diff --git a/dom/workers/test/referrer_test_server.sjs b/dom/workers/test/referrer_test_server.sjs new file mode 100644 index 0000000000..80bd2bee54 --- /dev/null +++ b/dom/workers/test/referrer_test_server.sjs @@ -0,0 +1,97 @@ +const SJS = "referrer_test_server.sjs?"; +const SHARED_KEY = SJS; + +var SAME_ORIGIN = "https://example.com/tests/dom/workers/test/" + SJS; +var CROSS_ORIGIN = "https://test2.example.com/tests/dom/workers/test/" + SJS; +var DOWNGRADE = "http://example.com/tests/dom/workers/test/" + SJS; + +function createUrl(aRequestType, aPolicy) { + var searchParams = new URLSearchParams(); + searchParams.append("ACTION", "request-worker"); + searchParams.append("Referrer-Policy", aPolicy); + searchParams.append("TYPE", aRequestType); + + var url = SAME_ORIGIN; + + if (aRequestType === "cross-origin") { + url = CROSS_ORIGIN; + } else if (aRequestType === "downgrade") { + url = DOWNGRADE; + } + + return url + searchParams.toString(); +} +function createWorker(aRequestType, aPolicy) { + return ` + onmessage = function() { + fetch("${createUrl(aRequestType, aPolicy)}").then(function () { + postMessage(42); + close(); + }); + } + `; +} + +function handleRequest(request, response) { + var params = new URLSearchParams(request.queryString); + var policy = params.get("Referrer-Policy"); + var type = params.get("TYPE"); + var action = params.get("ACTION"); + response.setHeader("Content-Security-Policy", "default-src *", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + if (policy) { + response.setHeader("Referrer-Policy", policy, false); + } + + if (action === "test") { + response.setHeader("Content-Type", "text/javascript", false); + response.write(createWorker(type, policy)); + return; + } + + if (action === "resetState") { + setSharedState(SHARED_KEY, "{}"); + response.write(""); + return; + } + + if (action === "get-test-results") { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write(getSharedState(SHARED_KEY)); + return; + } + + if (action === "request-worker") { + var result = getSharedState(SHARED_KEY); + result = result ? JSON.parse(result) : {}; + var referrerLevel = "none"; + var test = {}; + + if (request.hasHeader("Referer")) { + var referrer = request.getHeader("Referer"); + if (referrer.indexOf("referrer_test_server") > 0) { + referrerLevel = "full"; + } else if (referrer.indexOf("https://example.com") == 0) { + referrerLevel = "origin"; + } else { + // this is never supposed to happen + referrerLevel = "other-origin"; + } + test.referrer = referrer; + } else { + test.referrer = ""; + } + + test.policy = referrerLevel; + test.expected = policy; + + // test id equals type + "-" + policy + // Ex: same-origin-default + result[type + "-" + policy] = test; + setSharedState(SHARED_KEY, JSON.stringify(result)); + + response.write("'hello world'"); + } +} diff --git a/dom/workers/test/referrer_worker.html b/dom/workers/test/referrer_worker.html new file mode 100644 index 0000000000..f4c6719912 --- /dev/null +++ b/dom/workers/test/referrer_worker.html @@ -0,0 +1,144 @@ +<!DOCTYPE html> +<html> +<head> +</head> +<body onload="tests.next();"> +<script type="text/javascript"> +const SJS = "referrer_test_server.sjs?"; +const BASE_URL = "https://example.com/tests/dom/workers/test/" + SJS; +const GET_RESULT = BASE_URL + 'ACTION=get-test-results'; +const RESET_STATE = BASE_URL + 'ACTION=resetState'; + +function ok(val, message) { + val = val ? "true" : "false"; + window.parent.postMessage("SimpleTest.ok(" + val + ", '" + message + "');", "*"); +} + +function info(val) { + window.parent.postMessage("SimpleTest.info(" + val + ");", "*"); +} + +function is(a, b, message) { + ok(a == b, message); +} + +function finish() { + // Let window.onerror have a chance to fire + setTimeout(function() { + setTimeout(function() { + tests.return(); + window.parent.postMessage("SimpleTest.finish();", "*"); + }, 0); + }, 0); +} + +var testCases = { + 'same-origin': { 'Referrer-Policy' : { 'default' : 'full', + 'origin' : 'origin', + 'origin-when-cross-origin' : 'full', + 'unsafe-url' : 'full', + 'same-origin' : 'full', + 'strict-origin' : 'origin', + 'strict-origin-when-cross-origin' : 'full', + 'no-referrer' : 'none', + 'unsafe-url, no-referrer' : 'none', + 'invalid' : 'full' }}, + + 'cross-origin': { 'Referrer-Policy' : { 'default' : 'origin', + 'origin' : 'origin', + 'origin-when-cross-origin' : 'origin', + 'unsafe-url' : 'full', + 'same-origin' : 'none', + 'strict-origin' : 'origin', + 'strict-origin-when-cross-origin' : 'origin', + 'no-referrer' : 'none', + 'unsafe-url, no-referrer' : 'none', + 'invalid' : 'origin' }}, + + // Downgrading in worker is blocked entirely without unblock option + // https://bugzilla.mozilla.org/show_bug.cgi?id=1198078#c17 + // Skip the downgrading test + /* 'downgrade': { 'Referrer-Policy' : { 'default' : 'full', + 'origin' : 'full', + 'origin-when-cross-origin"' : 'full', + 'unsafe-url' : 'full', + 'same-origin' : 'none', + 'strict-origin' : 'none', + 'strict-origin-when-cross-origin' : 'none', + 'no-referrer' : 'full', + 'unsafe-url, no-referrer' : 'none', + 'invalid' : 'full' }}, */ + + +}; + +var advance = function() { tests.next(); }; + +/** + * helper to perform an XHR + * to do checkIndividualResults and resetState + */ +function doXHR(aUrl, onSuccess, onFail) { + var xhr = new XMLHttpRequest({mozSystem: true}); + xhr.responseType = "json"; + xhr.onload = function () { + onSuccess(xhr); + }; + xhr.onerror = function () { + onFail(xhr); + }; + xhr.open('GET', aUrl, true); + xhr.send(null); +} + + +function resetState() { + doXHR(RESET_STATE, + advance, + function(xhr) { + ok(false, "error in reset state"); + finish(); + }); +} + +function checkIndividualResults(aType, aPolicy, aExpected) { + var onload = xhr => { + var results = xhr.response; + dump(JSON.stringify(xhr.response)); + // test id equals type + "-" + policy + // Ex: same-origin-default + var id = aType + "-" + aPolicy; + ok(id in results, id + " tests have to be performed."); + is(results[id].policy, aExpected, id + ' --- ' + results[id].policy + ' (' + results[id].referrer + ')'); + advance(); + }; + var onerror = xhr => { + ok(false, "Can't get results from the counter server."); + finish(); + }; + doXHR(GET_RESULT, onload, onerror); +} + +var tests = (function*() { + + for (var type in testCases) { + for (var policy in testCases[type]['Referrer-Policy']) { + yield resetState(); + var searchParams = new URLSearchParams(); + searchParams.append("TYPE", type); + searchParams.append("ACTION", "test"); + searchParams.append("Referrer-Policy", policy); + var worker = new Worker(BASE_URL + searchParams.toString()); + worker.onmessage = function () { + advance(); + }; + yield worker.postMessage(42); + yield checkIndividualResults(type, policy, escape(testCases[type]['Referrer-Policy'][policy])); + } + } + + finish(); +})(); +</script> +</body> +</html> diff --git a/dom/workers/test/rvals_worker.js b/dom/workers/test/rvals_worker.js new file mode 100644 index 0000000000..221d701443 --- /dev/null +++ b/dom/workers/test/rvals_worker.js @@ -0,0 +1,13 @@ +onmessage = function (evt) { + postMessage(postMessage("ignore") == undefined); + + var id = setInterval(function () {}, 200); + postMessage(clearInterval(id) == undefined); + + id = setTimeout(function () {}, 200); + postMessage(clearTimeout(id) == undefined); + + postMessage(dump(42 + "\n") == undefined); + + postMessage("finished"); +}; diff --git a/dom/workers/test/script_createFile.js b/dom/workers/test/script_createFile.js new file mode 100644 index 0000000000..09f7be509e --- /dev/null +++ b/dom/workers/test/script_createFile.js @@ -0,0 +1,43 @@ +/* eslint-env mozilla/chrome-script */ + +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["File"]); + +addMessageListener("file.open", function (e) { + var tmpFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + tmpFile.append("file.txt"); + tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + File.createFromNsIFile(tmpFile).then(function (file) { + sendAsyncMessage("file.opened", { data: file }); + }); +}); + +addMessageListener("nonEmptyFile.open", function (e) { + var tmpFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + tmpFile.append("file.txt"); + tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + tmpFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + var fileData = "Hello world!"; + outStream.write(fileData, fileData.length); + outStream.close(); + + File.createFromNsIFile(tmpFile).then(function (file) { + sendAsyncMessage("nonEmptyFile.opened", { data: file }); + }); +}); diff --git a/dom/workers/test/server_fetch_synthetic.sjs b/dom/workers/test/server_fetch_synthetic.sjs new file mode 100644 index 0000000000..703b26d0d2 --- /dev/null +++ b/dom/workers/test/server_fetch_synthetic.sjs @@ -0,0 +1,50 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function log(str) { + //dump(`SJS LOG: ${str}\n`); +} + +/** + * Given a multipart/form-data encoded string that we know to have only a single + * part, return the contents of the part. (MIME multipart encoding is too + * exciting to delve into.) + */ +function extractBlobFromMultipartFormData(text) { + const lines = text.split(/\r\n/g); + const firstBlank = lines.indexOf(""); + const foo = lines.slice(firstBlank + 1, -2).join("\n"); + return foo; +} + +async function handleRequest(request, response) { + let blobContents = ""; + if (request.method !== "POST") { + } else { + var body = new BinaryInputStream(request.bodyInputStream); + + var avail; + var bytes = []; + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + let requestBodyContents = String.fromCharCode.apply(null, bytes); + log(requestBodyContents); + blobContents = extractBlobFromMultipartFormData(requestBodyContents); + } + + log("Setting Headers"); + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(request.httpVersion, "200", "OK"); + response.write(`<!DOCTYPE HTML><head><meta charset="utf-8"/></head><body> + <h1 id="url">${request.scheme}${request.host}${request.port}${request.path}</h1> + <div id="source">ServerJS</div> + <div id="blob">${blobContents}</div> + </body>`); + log("Done"); +} diff --git a/dom/workers/test/sharedWorker_console.js b/dom/workers/test/sharedWorker_console.js new file mode 100644 index 0000000000..d78fca94c6 --- /dev/null +++ b/dom/workers/test/sharedWorker_console.js @@ -0,0 +1,12 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +onconnect = function (evt) { + console.profile("Hello profiling from a SharedWorker!"); + console.log("Hello world from a SharedWorker!"); + console.log("Here is a SAB", new SharedArrayBuffer(1024)); + evt.ports[0].postMessage("ok!"); +}; diff --git a/dom/workers/test/sharedWorker_lifetime.js b/dom/workers/test/sharedWorker_lifetime.js new file mode 100644 index 0000000000..594bd5833d --- /dev/null +++ b/dom/workers/test/sharedWorker_lifetime.js @@ -0,0 +1,5 @@ +onconnect = function (e) { + setTimeout(function () { + e.ports[0].postMessage("Still alive!"); + }, 500); +}; diff --git a/dom/workers/test/sharedWorker_ports.js b/dom/workers/test/sharedWorker_ports.js new file mode 100644 index 0000000000..6bdb5695db --- /dev/null +++ b/dom/workers/test/sharedWorker_ports.js @@ -0,0 +1,30 @@ +var port; +onconnect = function (evt) { + evt.source.postMessage({ type: "connected" }); + + if (!port) { + port = evt.source; + evt.source.onmessage = function (evtFromPort) { + port.postMessage({ + type: "status", + test: "Port from the main-thread!" == evtFromPort.data, + msg: "The message is coming from the main-thread", + }); + port.postMessage({ + type: "status", + test: evtFromPort.ports.length == 1, + msg: "1 port transferred", + }); + + evtFromPort.ports[0].onmessage = function (evtFromPort2) { + port.postMessage({ + type: "status", + test: evtFromPort2.data.type == "connected", + msg: "The original message received", + }); + port.postMessage({ type: "finish" }); + close(); + }; + }; + } +}; diff --git a/dom/workers/test/sharedWorker_privateBrowsing.js b/dom/workers/test/sharedWorker_privateBrowsing.js new file mode 100644 index 0000000000..c16bd209f0 --- /dev/null +++ b/dom/workers/test/sharedWorker_privateBrowsing.js @@ -0,0 +1,4 @@ +var counter = 0; +onconnect = function (evt) { + evt.ports[0].postMessage(++counter); +}; diff --git a/dom/workers/test/sharedWorker_sharedWorker.js b/dom/workers/test/sharedWorker_sharedWorker.js new file mode 100644 index 0000000000..a7f859919f --- /dev/null +++ b/dom/workers/test/sharedWorker_sharedWorker.js @@ -0,0 +1,100 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +if (!("self" in this)) { + throw new Error("No 'self' exists on SharedWorkerGlobalScope!"); +} +if (this !== self) { + throw new Error("'self' not equal to global object!"); +} +if (!(self instanceof SharedWorkerGlobalScope)) { + throw new Error("self not a SharedWorkerGlobalScope instance!"); +} + +var propsToCheck = [ + "location", + "navigator", + "close", + "importScripts", + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "dump", + "atob", + "btoa", +]; + +for (var index = 0; index < propsToCheck.length; index++) { + var prop = propsToCheck[index]; + if (!(prop in self)) { + throw new Error("SharedWorkerGlobalScope has no '" + prop + "' property!"); + } +} + +onconnect = function (event) { + if (!("SharedWorkerGlobalScope" in self)) { + throw new Error("SharedWorkerGlobalScope should be visible!"); + } + if (!(self instanceof SharedWorkerGlobalScope)) { + throw new Error("The global should be a SharedWorkerGlobalScope!"); + } + if (!(self instanceof WorkerGlobalScope)) { + throw new Error("The global should be a WorkerGlobalScope!"); + } + if ("DedicatedWorkerGlobalScope" in self) { + throw new Error("DedicatedWorkerGlobalScope should not be visible!"); + } + if (!(event instanceof MessageEvent)) { + throw new Error("'connect' event is not a MessageEvent!"); + } + if (!("ports" in event)) { + throw new Error("'connect' event doesn't have a 'ports' property!"); + } + if (event.ports.length != 1) { + throw new Error( + "'connect' event has a 'ports' property with length '" + + event.ports.length + + "'!" + ); + } + if (!event.ports[0]) { + throw new Error("'connect' event has a null 'ports[0]' property!"); + } + if (!(event.ports[0] instanceof MessagePort)) { + throw new Error( + "'connect' event has a 'ports[0]' property that isn't a " + "MessagePort!" + ); + } + if (!(event.ports[0] == event.source)) { + throw new Error("'connect' event source property is incorrect!"); + } + if (event.data) { + throw new Error("'connect' event has data: " + event.data); + } + + // Statement after return should trigger a warning, but NOT fire error events + // at us. + (function () { + return; + // eslint-disable-next-line no-unreachable + 1; + }); + + event.ports[0].onmessage = function (msg) { + if (!(msg instanceof MessageEvent)) { + throw new Error("'message' event is not a MessageEvent!"); + } + if (!("ports" in msg)) { + throw new Error("'message' event doesn't have a 'ports' property!"); + } + if (msg.ports === null) { + throw new Error("'message' event has a null 'ports' property!"); + } + msg.target.postMessage(msg.data); + throw new Error(msg.data); + }; +}; diff --git a/dom/workers/test/sharedWorker_thirdparty_frame.html b/dom/workers/test/sharedWorker_thirdparty_frame.html new file mode 100644 index 0000000000..ebd78412a6 --- /dev/null +++ b/dom/workers/test/sharedWorker_thirdparty_frame.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<script> + let params = new URLSearchParams(document.location.search.substring(1)); + let name = params.get('name'); + try { + let worker = new SharedWorker('sharedWorker_sharedWorker.js', + { name }); + worker.port.addEventListener('message', evt => { + parent.postMessage( { name, result: 'allowed' }, '*'); + }, { once: true }); + worker.port.start(); + worker.port.postMessage('ping'); + } catch(e) { + parent.postMessage({ name, result: 'blocked' }, '*'); + } +</script> diff --git a/dom/workers/test/sharedWorker_thirdparty_window.html b/dom/workers/test/sharedWorker_thirdparty_window.html new file mode 100644 index 0000000000..f5f01066c2 --- /dev/null +++ b/dom/workers/test/sharedWorker_thirdparty_window.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SharedWorker in 3rd Party Iframes</title> +</head> +<body> + <script> + + let url = new URL(window.location); + + let frame = document.createElement('iframe'); + frame.src = + 'http://example.org/tests/dom/workers/test/sharedWorker_thirdparty_frame.html?name=' + url.searchParams.get('name'); + document.body.appendChild(frame); + window.addEventListener('message', evt => { + frame.remove(); + opener.postMessage(evt.data, "*"); + }, {once: true}); + + </script> +</body> +</html> diff --git a/dom/workers/test/simpleThread_worker.js b/dom/workers/test/simpleThread_worker.js new file mode 100644 index 0000000000..9d6fb30f2f --- /dev/null +++ b/dom/workers/test/simpleThread_worker.js @@ -0,0 +1,52 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +function messageListener(event) { + var exception; + try { + event.bubbles = true; + } catch (e) { + exception = e; + } + + if (!(exception instanceof TypeError)) { + throw exception; + } + + switch (event.data) { + case "no-op": + break; + case "components": + postMessage(Components.toString()); + break; + case "start": + for (var i = 0; i < 1000; i++) {} + postMessage("started"); + break; + case "stop": + self.postMessage("no-op"); + postMessage("stopped"); + self.removeEventListener("message", messageListener); + break; + default: + throw "Bad message: " + event.data; + } +} + +if (!("DedicatedWorkerGlobalScope" in self)) { + throw new Error("DedicatedWorkerGlobalScope should be visible!"); +} +if (!(self instanceof DedicatedWorkerGlobalScope)) { + throw new Error("The global should be a SharedWorkerGlobalScope!"); +} +if (!(self instanceof WorkerGlobalScope)) { + throw new Error("The global should be a WorkerGlobalScope!"); +} +if ("SharedWorkerGlobalScope" in self) { + throw new Error("SharedWorkerGlobalScope should not be visible!"); +} + +addEventListener("message", { handleEvent: messageListener }); diff --git a/dom/workers/test/sourcemap_header.js b/dom/workers/test/sourcemap_header.js new file mode 100644 index 0000000000..9f10b35ed9 --- /dev/null +++ b/dom/workers/test/sourcemap_header.js @@ -0,0 +1,65 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +(async () => { + SimpleTest.waitForExplicitFinish(); + + const HTTP_BASE_URL = "http://mochi.test:8888/tests/dom/workers/test/"; + const IFRAME_URL = HTTP_BASE_URL + "sourcemap_header_iframe.html"; + const WORKER_URL = HTTP_BASE_URL + "sourcemap_header_worker.js"; + const DEBUGGER_URL = BASE_URL + "sourcemap_header_debugger.js"; + + const workerFrame = document.getElementById("worker-frame"); + ok(workerFrame, "has frame"); + + await new Promise(r => { + workerFrame.onload = r; + workerFrame.src = IFRAME_URL; + }); + + info("Start worker and watch for registration"); + const workerLoadedChannel = new MessageChannel(); + + const loadDebuggerAndWorker = Promise.all([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + // We need to wait for the worker to load so a Debugger.Source will be + // guaranteed to exist. + new Promise(r => { + workerLoadedChannel.port1.onmessage = r; + }), + ]); + workerFrame.contentWindow.postMessage(WORKER_URL, "*", [ + workerLoadedChannel.port2, + ]); + const [dbg] = await loadDebuggerAndWorker; + + // Wait for the debugger server to reply with the sourceMapURL of the + // loaded worker scripts. + info("Querying for the sourceMapURL of the worker script"); + const urls = await new Promise(res => { + dbg.addListener({ + onMessage(msg) { + const data = JSON.parse(msg); + if (data.type !== "response-sourceMapURL") { + return; + } + dbg.removeListener(this); + res(data.value); + }, + }); + dbg.postMessage( + JSON.stringify({ + type: "request-sourceMapURL", + url: WORKER_URL, + }) + ); + }); + + ok(Array.isArray(urls) && urls.length === 1, "has a single source actor"); + is(urls[0], "worker-header.js.map", "has the right map URL"); + + SimpleTest.finish(); +})(); diff --git a/dom/workers/test/sourcemap_header_debugger.js b/dom/workers/test/sourcemap_header_debugger.js new file mode 100644 index 0000000000..bb8ed0c1f7 --- /dev/null +++ b/dom/workers/test/sourcemap_header_debugger.js @@ -0,0 +1,29 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +addEventListener("message", function (event) { + let data; + try { + data = JSON.parse(event.data); + } catch {} + + switch (data.type) { + case "request-sourceMapURL": + const dbg = new Debugger(global); + const sourceMapURLs = dbg + .findSources() + .filter(source => source.url === data.url) + .map(source => source.sourceMapURL); + + postMessage( + JSON.stringify({ + type: "response-sourceMapURL", + value: sourceMapURLs, + }) + ); + break; + } +}); diff --git a/dom/workers/test/sourcemap_header_iframe.html b/dom/workers/test/sourcemap_header_iframe.html new file mode 100644 index 0000000000..82278f41a1 --- /dev/null +++ b/dom/workers/test/sourcemap_header_iframe.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <script> + self.onmessage = (msg) => { + const workerLoadedPort = msg.ports[0]; + const worker = new Worker(msg.data); + worker.onmessage = () => { + workerLoadedPort.postMessage("worker loaded"); + }; + }; + </script> +</head> +<body></body> +</html> diff --git a/dom/workers/test/sourcemap_header_worker.js b/dom/workers/test/sourcemap_header_worker.js new file mode 100644 index 0000000000..ca094686d3 --- /dev/null +++ b/dom/workers/test/sourcemap_header_worker.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Send a message pack so the test knows that the source has loaded before +// it tries to search for the Debugger.Source. +postMessage("loaded"); diff --git a/dom/workers/test/sourcemap_header_worker.js^headers^ b/dom/workers/test/sourcemap_header_worker.js^headers^ new file mode 100644 index 0000000000..833288ef02 --- /dev/null +++ b/dom/workers/test/sourcemap_header_worker.js^headers^ @@ -0,0 +1 @@ +X-SourceMap: worker-header.js.map diff --git a/dom/workers/test/suspend_blank.html b/dom/workers/test/suspend_blank.html new file mode 100644 index 0000000000..b6dd0075e6 --- /dev/null +++ b/dom/workers/test/suspend_blank.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<script> + var interval; + var finish = false; + var bc = new BroadcastChannel("suspendBlank"); + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "navigateBack") { + finish = true; + history.back(); + } + } + window.onpagehide = () => { + bc.postMessage({command: "pagehide"}); + if (finish) { + bc.close(); + } + } + window.onload = () => { + bc.postMessage({command: "loaded"}); + } +</script> diff --git a/dom/workers/test/suspend_window.html b/dom/workers/test/suspend_window.html new file mode 100644 index 0000000000..9bede3aaa6 --- /dev/null +++ b/dom/workers/test/suspend_window.html @@ -0,0 +1,82 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for DOM Worker Threads Suspending</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<div id="output"></div> +<script class="testbody" type="text/javascript"> + + var worker; + var finish = false; + var bc = new BroadcastChannel("suspendWindow"); + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "startWorker") { + startWorker(); + } else if (command == "navigate") { + window.location = "suspend_blank.html"; + } else if (command == "finish") { + finish = true; + terminateWorker(); + bc.postMessage({command: "finished"}); + bc.close(); + window.close(); + } + } + + function messageCallback(data) { + if (finish) { + return; + } + bc.postMessage({command: "messageCallback", data}); + } + + function errorCallback(msg) { + if (finish) { + return; + } + bc.postMessage({command: "errorCallback", data: msg}); + } + + var output = document.getElementById("output"); + + function terminateWorker() { + if (worker) { + worker.postMessage("stop"); + worker = null; + } + } + + function startWorker() { + var lastData; + worker = new Worker("suspend_worker.js"); + + worker.onmessage = function(event) { + output.textContent = (lastData ? lastData + " -> " : "") + event.data; + lastData = event.data; + messageCallback(event.data); + }; + + worker.onerror = function(event) { + this.terminate(); + errorCallback(event.message); + }; + } + + window.onload = () => { + bc.postMessage({command: "loaded"}); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/suspend_worker.js b/dom/workers/test/suspend_worker.js new file mode 100644 index 0000000000..e024972737 --- /dev/null +++ b/dom/workers/test/suspend_worker.js @@ -0,0 +1,13 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var counter = 0; + +var interval = setInterval(function () { + postMessage(++counter); +}, 100); + +onmessage = function (event) { + clearInterval(interval); +}; diff --git a/dom/workers/test/terminate_worker.js b/dom/workers/test/terminate_worker.js new file mode 100644 index 0000000000..7b9984e869 --- /dev/null +++ b/dom/workers/test/terminate_worker.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function (event) { + throw "No messages should reach me!"; +}; + +setInterval(function () { + postMessage("Still alive!"); +}, 100); diff --git a/dom/workers/test/test_404.html b/dom/workers/test/test_404.html new file mode 100644 index 0000000000..59ab691e02 --- /dev/null +++ b/dom/workers/test/test_404.html @@ -0,0 +1,39 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("nonexistent_worker.js"); + + worker.onmessage = function(event) { + ok(false, "Shouldn't ever get a message!"); + SimpleTest.finish(); + } + + worker.onerror = function(event) { + is(event.target, worker); + event.preventDefault(); + SimpleTest.finish(); + }; + + worker.postMessage("dummy"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_WorkerDebugger.initialize.xhtml b/dom/workers/test/test_WorkerDebugger.initialize.xhtml new file mode 100644 index 0000000000..9e7ec2e9a6 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger.initialize.xhtml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger.initialize" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger.initialize_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger.initialize_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger.initialize_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + await promise; + + info("Check that the debuggers are initialized before the workers " + + "start running."); + await waitForMultiple([ + waitForWorkerMessage(worker, "debugger"), + waitForWorkerMessage(worker, "worker"), + waitForWorkerMessage(worker, "child:debugger"), + waitForWorkerMessage(worker, "child:worker") + ]); + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger.postMessage.xhtml b/dom/workers/test/test_WorkerDebugger.postMessage.xhtml new file mode 100644 index 0000000000..58e15b3a05 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger.postMessage.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger.postMessage" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger.postMessage_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger.postMessage_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger.postMessage_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = await promise; + + info("Send a request to the worker debugger. This should cause the " + + "the worker debugger to send a response."); + promise = waitForDebuggerMessage(dbg, "pong"); + dbg.postMessage("ping"); + await promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to send a response."); + promise = waitForDebuggerMessage(childDbg, "pong"); + childDbg.postMessage("ping"); + await promise; + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger.xhtml b/dom/workers/test/test_WorkerDebugger.xhtml new file mode 100644 index 0000000000..d0810b851a --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger.xhtml @@ -0,0 +1,145 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger_childWorker.js"; + const SHARED_WORKER_URL = "WorkerDebugger_sharedWorker.js"; + + add_task( + async function runTest() { + info("Create a top-level chrome worker that creates a non-top-level " + + "content worker and wait for their debuggers to be registered."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL), + waitForRegister(CHILD_WORKER_URL) + ]); + worker = new ChromeWorker(WORKER_URL); + let [dbg, childDbg] = await promise; + + info("Check that the top-level chrome worker debugger has the " + + "correct properties."); + is(dbg.isChrome, true, + "Chrome worker debugger should be chrome."); + is(dbg.parent, null, + "Top-level debugger should not have parent."); + is(dbg.type, Ci.nsIWorkerDebugger.TYPE_DEDICATED, + "Chrome worker debugger should be dedicated."); + is(dbg.window, window, + "Top-level dedicated worker debugger should have window."); + + info("Check that the non-top-level content worker debugger has the " + + "correct properties."); + is(childDbg.isChrome, false, + "Content worker debugger should be content."); + is(childDbg.parent, dbg, + "Non-top-level worker debugger should have parent."); + is(childDbg.type, Ci.nsIWorkerDebugger.TYPE_DEDICATED, + "Content worker debugger should be dedicated."); + is(childDbg.window, window, + "Non-top-level worker debugger should have window."); + + info("Terminate the top-level chrome worker and the non-top-level " + + "content worker, and wait for their debuggers to be " + + "unregistered and closed."); + promise = waitForMultiple([ + waitForUnregister(CHILD_WORKER_URL), + waitForDebuggerClose(childDbg), + waitForUnregister(WORKER_URL), + waitForDebuggerClose(dbg), + ]); + worker.terminate(); + await promise; + + info("Create a shared worker and wait for its debugger to be " + + "registered"); + promise = waitForRegister(SHARED_WORKER_URL); + worker = new SharedWorker(SHARED_WORKER_URL); + let sharedDbg = await promise; + + info("Check that the shared worker debugger has the correct " + + "properties."); + is(sharedDbg.isChrome, false, + "Shared worker debugger should be content."); + is(sharedDbg.parent, null, + "Shared worker debugger should not have parent."); + is(sharedDbg.type, Ci.nsIWorkerDebugger.TYPE_SHARED, + "Shared worker debugger should be shared."); + is(sharedDbg.window, null, + "Shared worker debugger should not have window."); + + info("Create a shared worker with the same URL and check that its " + + "debugger is not registered again."); + let listener = { + onRegistered () { + ok(false, + "Shared worker debugger should not be registered again."); + }, + }; + wdm.addListener(listener); + + let secondWorker = new SharedWorker(SHARED_WORKER_URL); + + info("Send a message to the shared worker to tell it to close " + + "itself, and wait for its debugger to be closed."); + promise = waitForMultiple([ + waitForUnregister(SHARED_WORKER_URL), + waitForDebuggerClose(sharedDbg) + ]); + secondWorker.port.start(); + secondWorker.port.postMessage("close"); + await promise; + worker = null; + secondWorker = null; + + info("Create a SharedWorker again for the infinite loop test.") + promise = waitForRegister(SHARED_WORKER_URL); + // Give it an explicit name so we don't reuse the above SharedWorker. + worker = new SharedWorker(SHARED_WORKER_URL, "loopy"); + sharedDbg = await promise; + + info("Send a message to the shared worker to tell it to close " + + "itself, then loop forever, and wait for its debugger to be closed."); + promise = waitForMultiple([ + waitForUnregister(SHARED_WORKER_URL), + waitForDebuggerClose(sharedDbg) + ]); + + // When the closing process begins, we schedule a timer to terminate + // the worker in case it's in an infinite loop, which is exactly what + // we do in this test. The default delay is 30 seconds. This test + // previously waited 15 seconds for reasons that were poorly justified. + // We now set it to 100ms because we just want to make sure that the + // timeout mechanism to force cancellation from the parent properly + // works (as long as the parent thread isn't blocked). + await SpecialPowers.pushPrefEnv({"set": [[ "dom.worker.canceling.timeoutMilliseconds", 100 ]]}); + + worker.port.start(); + worker.port.postMessage("close_loop"); + await promise; + + wdm.removeListener(listener); + } + ); + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.createSandbox.xhtml b/dom/workers/test/test_WorkerDebuggerGlobalScope.createSandbox.xhtml new file mode 100644 index 0000000000..89114f5e49 --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.createSandbox.xhtml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.createSandbox" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.createSandbox_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.createSandbox_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker, wait for its debugger to be registered, and " + + "initialize it."); + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = await promise; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to send a response from within a sandbox."); + promise = waitForDebuggerMessage(dbg, "pong"); + dbg.postMessage("ping"); + await promise; + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.enterEventLoop.xhtml b/dom/workers/test/test_WorkerDebuggerGlobalScope.enterEventLoop.xhtml new file mode 100644 index 0000000000..d5cff95d39 --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.enterEventLoop.xhtml @@ -0,0 +1,124 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.enterEventLoop" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.enterEventLoop_worker.js"; + const CHILD_WORKER_URL = "WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.enterEventLoop_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = await promise; + + info("Send a request to the child worker. This should cause the " + + "child worker debugger to enter a nested event loop."); + promise = waitForDebuggerMessage(childDbg, "paused"); + worker.postMessage("child:ping"); + await promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to enter a second nested event loop."); + promise = waitForDebuggerMessage(childDbg, "paused"); + childDbg.postMessage("eval"); + await promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to leave its second nested event " + + "loop. The child worker debugger should not send a response " + + "for its previous request until after it has left the nested " + + "event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(childDbg, "resumed"), + waitForDebuggerMessage(childDbg, "evalled") + ]); + childDbg.postMessage("resume"); + await promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to leave its first nested event loop." + + "The child worker should not send a response for its earlier " + + "request until after the child worker debugger has left the " + + "nested event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(childDbg, "resumed"), + waitForWorkerMessage(worker, "child:pong") + ]); + childDbg.postMessage("resume"); + await promise; + + info("Send a request to the worker. This should cause the worker " + + "debugger to enter a nested event loop."); + promise = waitForDebuggerMessage(dbg, "paused"); + worker.postMessage("ping"); + await promise; + + info("Terminate the worker. This should not cause the worker " + + "debugger to terminate as well."); + worker.terminate(); + + worker.onmessage = function () { + ok(false, "Worker should have been terminated."); + }; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to enter a second nested event loop."); + promise = waitForDebuggerMessage(dbg, "paused"); + dbg.postMessage("eval"); + await promise; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to leave its second nested event loop. The " + + "worker debugger should not send a response for the previous " + + "request until after leaving the nested event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "resumed"), + waitForDebuggerMessage(dbg, "evalled") + ]); + dbg.postMessage("resume"); + await promise; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to leave its first nested event loop. The " + + "worker should not send a response for its earlier request, " + + "since it has been terminated."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "resumed"), + ]); + dbg.postMessage("resume"); + await promise; + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.reportError.xhtml b/dom/workers/test/test_WorkerDebuggerGlobalScope.reportError.xhtml new file mode 100644 index 0000000000..20e731c1cf --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.reportError.xhtml @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.reportError" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.reportError_worker.js"; + const CHILD_WORKER_URL = "WorkerDebuggerGlobalScope.reportError_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.reportError_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = await promise; + + worker.onmessage = function () { + ok(false, "Debugger error events should not be fired at workers."); + }; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to report an error."); + promise = waitForDebuggerError(dbg); + dbg.postMessage("report"); + let error = await promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is reported."); + is(error.lineNumber, 6, + "lineNumber should be line number from which error is reported."); + is(error.message, "reported", "message should be reported."); + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to throw an error."); + promise = waitForDebuggerError(dbg); + dbg.postMessage("throw"); + error = await promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is thrown"); + is(error.lineNumber, 9, + "lineNumber should be line number from which error is thrown"); + is(error.message, "Error: thrown", "message should be Error: thrown"); + + info("Send a reqeust to the child worker debugger. This should cause " + + "the child worker debugger to report an error."); + promise = waitForDebuggerError(childDbg); + childDbg.postMessage("report"); + error = await promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is reported."); + is(error.lineNumber, 6, + "lineNumber should be line number from which error is reported."); + is(error.message, "reported", "message should be reported."); + + info("Send a message to the child worker debugger. This should cause " + + "the child worker debugger to throw an error."); + promise = waitForDebuggerError(childDbg); + childDbg.postMessage("throw"); + error = await promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is thrown"); + is(error.lineNumber, 9, + "lineNumber should be line number from which error is thrown"); + is(error.message, "Error: thrown", "message should be Error: thrown"); + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.setImmediate.xhtml b/dom/workers/test/test_WorkerDebuggerGlobalScope.setImmediate.xhtml new file mode 100644 index 0000000000..4b09f01708 --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.setImmediate.xhtml @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.setImmediate" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.setImmediate_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.setImmediate_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = await promise; + + info("Send a request to the worker debugger. This should cause a " + + "the worker debugger to send two responses. The worker debugger " + + "should send the second response before the first one, since " + + "the latter is delayed until the next tick of the event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "pong2"), + waitForDebuggerMessage(dbg, "pong1") + ]); + dbg.postMessage("ping"); + await promise; + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerManager.xhtml b/dom/workers/test/test_WorkerDebuggerManager.xhtml new file mode 100644 index 0000000000..1ed09563de --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerManager.xhtml @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerManager_worker.js"; + const CHILD_WORKER_URL = "WorkerDebuggerManager_childWorker.js"; + + add_task( + async function runTest() { + info("Check that worker debuggers are not enumerated before they are " + + "registered."); + ok(!findDebugger(WORKER_URL), + "Worker debugger should not be enumerated before it is registered."); + ok(!findDebugger(CHILD_WORKER_URL), + "Child worker debugger should not be enumerated before it is " + + "registered."); + + info("Create a worker that creates a child worker, and wait for " + + "their debuggers to be registered."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL), + waitForRegister(CHILD_WORKER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = await promise; + + info("Check that worker debuggers are enumerated after they are " + + "registered."); + ok(findDebugger(WORKER_URL), + "Worker debugger should be enumerated after it is registered."); + ok(findDebugger(CHILD_WORKER_URL), + "Child worker debugger should be enumerated after it is " + + "registered."); + + info("Check that worker debuggers are not closed before they are " + + "unregistered."); + is(dbg.isClosed, false, + "Worker debugger should not be closed before it is unregistered."); + is(childDbg.isClosed, false, + "Child worker debugger should not be closed before it is " + + "unregistered"); + + info("Terminate the worker and the child worker, and wait for their " + + "debuggers to be unregistered."); + promise = waitForMultiple([ + waitForUnregister(CHILD_WORKER_URL), + waitForUnregister(WORKER_URL), + ]); + worker.terminate(); + await promise; + + info("Check that worker debuggers are not enumerated after they are " + + "unregistered."); + ok(!findDebugger(WORKER_URL), + "Worker debugger should not be enumerated after it is " + + "unregistered."); + ok(!findDebugger(CHILD_WORKER_URL), + "Child worker debugger should not be enumerated after it is " + + "unregistered."); + + info("Check that worker debuggers are closed after they are " + + "unregistered."); + is(dbg.isClosed, true, + "Worker debugger should be closed after it is unregistered."); + is(childDbg.isClosed, true, + "Child worker debugger should be closed after it is unregistered."); + + info("Check that property accesses on worker debuggers throws " + + "after they are closed."); + assertThrows(() => dbg.url, + "Property accesses on worker debugger should throw " + + "after it is closed."); + assertThrows(() => childDbg.url, + "Property accesses on child worker debugger should " + + "throw after it is closed."); + } + ); + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_console.xhtml b/dom/workers/test/test_WorkerDebugger_console.xhtml new file mode 100644 index 0000000000..b38ccdac45 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_console.xhtml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.console methods" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger.console_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger.console_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger.console_debugger.js"; + + consoleMessagesReceived = 0; + function test() { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + function consoleListener() { + this.observe = this.observe.bind(this); + ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal); + } + + consoleListener.prototype = { + observe(aSubject) { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == "Hello from the debugger script!" && + !consoleMessagesReceived) { + consoleMessagesReceived++; + ok(true, "Something has been received"); + ConsoleAPIStorage.removeLogEventListener(this.observe); + } + } + } + + var cl = new consoleListener(); + + (async function() { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = await promise; + + info("Send a request to the worker debugger. This should cause the " + + "the worker debugger to send a response."); + dbg.addListener({ + onMessage(msg) { + try { + msg = JSON.parse(msg); + } catch(e) { + ok(false, "Something went wrong"); + return; + } + + if (msg.type == 'finish') { + ok(consoleMessagesReceived, "We received something via debugger console!"); + dbg.removeListener(this); + SimpleTest.finish(); + return; + } + + if (msg.type == 'status') { + ok(msg.what, msg.msg); + return; + } + + ok(false, "Something went wrong"); + } + }); + + dbg.postMessage("do magic"); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_frozen.xhtml b/dom/workers/test/test_WorkerDebugger_frozen.xhtml new file mode 100644 index 0000000000..f29def78ce --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_frozen.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger with frozen workers" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WINDOW1_URL = "WorkerDebugger_frozen_window1.html"; + const WINDOW2_URL = "WorkerDebugger_frozen_window2.html"; + + const WORKER1_URL = "WorkerDebugger_frozen_worker1.js"; + const WORKER2_URL = "WorkerDebugger_frozen_worker2.js"; + + add_task( + async function runTest() { + await SpecialPowers.pushPrefEnv({set: + [["browser.sessionhistory.max_total_viewers", 10]]}); + + let promise = waitForMultiple([ + waitForRegister(WORKER1_URL), + waitForWindowMessage(window, "ready"), + ]); + let testWin = window.open(WINDOW1_URL, "testWin");; + let [dbg1] = await promise; + is(dbg1.isClosed, false, + "debugger for worker on page 1 should not be closed"); + + promise = waitForMultiple([ + waitForUnregister(WORKER1_URL), + waitForDebuggerClose(dbg1), + waitForRegister(WORKER2_URL), + waitForWindowMessage(window, "ready"), + ]); + testWin.location = WINDOW2_URL; + let [,, dbg2] = await promise; + is(dbg1.isClosed, true, + "debugger for worker on page 1 should be closed"); + is(dbg2.isClosed, false, + "debugger for worker on page 2 should not be closed"); + + promise = Promise.all([ + waitForUnregister(WORKER2_URL), + waitForDebuggerClose(dbg2), + waitForRegister(WORKER1_URL) + ]); + testWin.history.back(); + [,, dbg1] = await promise; + is(dbg1.isClosed, false, + "debugger for worker on page 1 should not be closed"); + is(dbg2.isClosed, true, + "debugger for worker on page 2 should be closed"); + } + ); + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_promise.xhtml b/dom/workers/test/test_WorkerDebugger_promise.xhtml new file mode 100644 index 0000000000..14d50969b5 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_promise.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. ++ http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger with DOM Promises" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger_promise_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger_promise_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = await promise; + + info("Send a request to the worker. This should cause the worker " + + "to send a response."); + promise = waitForWorkerMessage(worker, "resolved"); + worker.postMessage("resolve"); + await promise; + + info("Send a request to the debugger. This should cause the debugger " + + "to send a response."); + promise = waitForDebuggerMessage(dbg, "resolved"); + dbg.postMessage("resolve"); + await promise; + + info("Send a request to the worker. This should cause the debugger " + + "to enter a nested event loop."); + promise = waitForDebuggerMessage(dbg, "paused"); + worker.postMessage("pause"); + await promise; + + info("Send a request to the debugger. This should cause the debugger " + + "to leave the nested event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "resumed"), + waitForWorkerMessage(worker, "resumed") + ]); + dbg.postMessage("resume"); + await promise; + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_suspended.xhtml b/dom/workers/test/test_WorkerDebugger_suspended.xhtml new file mode 100644 index 0000000000..d0c9bff552 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_suspended.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger with suspended workers" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger_suspended_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger_suspended_debugger.js"; + + function test() { + (async function() { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker, wait for its debugger to be registered, and " + + "initialize it."); + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = await promise; + + info("Send a request to the worker. This should cause both the " + + "worker and the worker debugger to send a response."); + promise = waitForMultiple([ + waitForWorkerMessage(worker, "worker"), + waitForDebuggerMessage(dbg, "debugger") + ]); + worker.postMessage("ping"); + await promise; + + info("Suspend the workers for this window, and send another request " + + "to the worker. This should cause only the worker debugger to " + + "send a response."); + let windowUtils = window.windowUtils; + windowUtils.suspendTimeouts(); + function onmessage() { + ok(false, "The worker should not send a response."); + }; + worker.addEventListener("message", onmessage); + promise = waitForDebuggerMessage(dbg, "debugger"); + worker.postMessage("ping"); + await promise; + worker.removeEventListener("message", onmessage); + + info("Resume the workers for this window. This should cause the " + + "worker to send a response to the previous request."); + promise = waitForWorkerMessage(worker, "worker"); + windowUtils.resumeTimeouts(); + await promise; + + SimpleTest.finish(); + })(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_atob.html b/dom/workers/test/test_atob.html new file mode 100644 index 0000000000..0e82029b46 --- /dev/null +++ b/dom/workers/test/test_atob.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script src="atob_worker.js" language="javascript"></script> +<script class="testbody" type="text/javascript"> + + var dataIndex = 0; + + var worker = new Worker("atob_worker.js"); + worker.onmessage = function(event) { + switch (event.data.type) { + case "done": + is(dataIndex, data.length, "Saw all values"); + SimpleTest.finish(); + return; + case "btoa": + is(btoa(data[dataIndex]), event.data.value, + "Good btoa value " + dataIndex); + break; + case "atob": + is(atob(btoa(data[dataIndex])) + "", event.data.value, + "Good round trip value " + dataIndex); + dataIndex++; + break; + default: + ok(false, "Worker posted a bad message: " + event.message); + worker.terminate(); + SimpleTest.finish(); + } + } + + worker.onerror = function(event) { + ok(false, "Worker threw an error: " + event.message); + worker.terminate(); + SimpleTest.finish(); + } + + worker.postMessage("go"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_blobConstructor.html b/dom/workers/test/test_blobConstructor.html new file mode 100644 index 0000000000..4aff5b545b --- /dev/null +++ b/dom/workers/test/test_blobConstructor.html @@ -0,0 +1,60 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<html> +<!-- +Tests of DOM Worker Blob constructor +--> +<head> + <title>Test for DOM Worker Blob constructor</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +(function() { + onerror = function(e) { + ok(false, "Main Thread had an error: " + event.data); + SimpleTest.finish(); + }; + function f() { + onmessage = function(e) { + var b = new Blob([e.data, "World"],{type: "text/plain"}); + var fr = new FileReaderSync(); + postMessage({text: fr.readAsText(b), type: b.type}); + }; + } + var b = new Blob([f,"f();"]); + var u = URL.createObjectURL(b); + var w = new Worker(u); + w.onmessage = function(e) { + URL.revokeObjectURL(u); + is(e.data.text, fr.result); + is(e.data.type, "text/plain"); + SimpleTest.finish(); + }; + w.onerror = function(e) { + is(e.target, w); + ok(false, "Worker had an error: " + e.message); + SimpleTest.finish(); + }; + + b = new Blob(["Hello, "]); + var fr = new FileReader(); + fr.readAsText(new Blob([b, "World"],{})); + fr.onload = function() { + w.postMessage(b); + }; + SimpleTest.waitForExplicitFinish(); +})(); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_blobWorkers.html b/dom/workers/test/test_blobWorkers.html new file mode 100644 index 0000000000..6ecd6c6f4b --- /dev/null +++ b/dom/workers/test/test_blobWorkers.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const message = "hi"; + + const workerScript = + "onmessage = function(event) {" + + " postMessage(event.data);" + + "};"; + + var worker = new Worker(URL.createObjectURL(new Blob([workerScript]))); + worker.onmessage = function(event) { + is(event.data, message, "Got correct message"); + SimpleTest.finish(); + }; + worker.postMessage(message); + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_bug1002702.html b/dom/workers/test/test_bug1002702.html new file mode 100644 index 0000000000..c020dcfd05 --- /dev/null +++ b/dom/workers/test/test_bug1002702.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1002702</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +var port = new SharedWorker('data:application/javascript,1').port; +port.close(); +SpecialPowers.forceGC(); +ok(true, "No crash \\o/"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1010784.html b/dom/workers/test/test_bug1010784.html new file mode 100644 index 0000000000..3e2f62971d --- /dev/null +++ b/dom/workers/test/test_bug1010784.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1010784 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1010784</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1010784">Mozilla Bug 1010784</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + + var worker = new Worker("file_bug1010784_worker.js"); + + worker.onmessage = function(event) { + is(event.data, "done", "Got correct result"); + SimpleTest.finish(); + } + + worker.postMessage("testXHR.txt"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1014466.html b/dom/workers/test/test_bug1014466.html new file mode 100644 index 0000000000..26ef2fb316 --- /dev/null +++ b/dom/workers/test/test_bug1014466.html @@ -0,0 +1,42 @@ +<!-- +2 Any copyright is dedicated to the Public Domain. +3 http://creativecommons.org/publicdomain/zero/1.0/ +4 --> +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1014466 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1014466</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1014466">Mozilla Bug 1014466</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + + var worker = new Worker("bug1014466_worker.js"); + + worker.onmessage = function(event) { + if (event.data.type == 'finish') { + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } + }; + + worker.postMessage(true); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1020226.html b/dom/workers/test/test_bug1020226.html new file mode 100644 index 0000000000..1ed69db41e --- /dev/null +++ b/dom/workers/test/test_bug1020226.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1020226 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1020226</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1020226">Mozilla Bug 1020226</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<iframe id="iframe" src="bug1020226_frame.html" onload="finishTest();"> +</iframe> +</div> +<pre id="test"> +<script type="application/javascript"> +function finishTest() { + document.getElementById("iframe").onload = null; + window.onmessage = function(e) { + info("Got message"); + document.getElementById("iframe").src = "about:blank"; + // We aren't really interested in the test, it shouldn't crash when the + // worker is GCed later. + ok(true, "Should not crash"); + SimpleTest.finish(); + }; +} + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1036484.html b/dom/workers/test/test_bug1036484.html new file mode 100644 index 0000000000..feada50f5a --- /dev/null +++ b/dom/workers/test/test_bug1036484.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads: bug 1036484 +--> +<head> + <title>Test for DOM Worker Threads: bug 1036484</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function test(script) { + var worker = new Worker(script); + + worker.onmessage = function(event) { + ok(false, "Shouldn't ever get a message!"); + } + + worker.onerror = function(event) { + is(event.target, worker); + event.preventDefault(); + runTests(); + }; + + worker.postMessage("dummy"); +} + +var tests = [ '404_server.sjs', '404_server.sjs?js' ]; +function runTests() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var script = tests.shift(); + test(script); +} + +SimpleTest.waitForExplicitFinish(); +runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1060621.html b/dom/workers/test/test_bug1060621.html new file mode 100644 index 0000000000..d2af81c93e --- /dev/null +++ b/dom/workers/test/test_bug1060621.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for URLSearchParams object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("bug1060621_worker.js"); + + worker.onmessage = function(event) { + ok(true, "The operation is done. We should not leak."); + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1062920.html b/dom/workers/test/test_bug1062920.html new file mode 100644 index 0000000000..bea2b7f461 --- /dev/null +++ b/dom/workers/test/test_bug1062920.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for navigator property override</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function checkValues() { + var worker = new Worker("bug1062920_worker.js"); + + worker.onmessage = function(event) { + var ifr = document.createElement('IFRAME'); + ifr.src = "about:blank"; + + ifr.addEventListener('load', function() { + var nav = ifr.contentWindow.navigator; + is(event.data.appCodeName, nav.appCodeName, "appCodeName should match"); + is(event.data.appName, nav.appName, "appName should match"); + is(event.data.appVersion, nav.appVersion, "appVersion should match"); + is(event.data.platform, nav.platform, "platform should match"); + is(event.data.userAgent, nav.userAgent, "userAgent should match"); + is(event.data.product, nav.product, "product should match"); + runTests(); + }); + + document.getElementById('content').appendChild(ifr); + }; + } + + function replaceAndCheckValues() { + SpecialPowers.pushPrefEnv({"set": [ + ["general.appversion.override", "appVersion overridden"], + ["general.platform.override", "platform overridden"], + ["general.useragent.override", "userAgent overridden"] + ]}, checkValues); + } + + var tests = [ + checkValues, + replaceAndCheckValues + ]; + + function runTests() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1062920.xhtml b/dom/workers/test/test_bug1062920.xhtml new file mode 100644 index 0000000000..0dfeccfb77 --- /dev/null +++ b/dom/workers/test/test_bug1062920.xhtml @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + + function checkValues() { + var worker = new Worker("bug1062920_worker.js"); + + worker.onmessage = function(event) { + is(event.data.appCodeName, navigator.appCodeName, "appCodeName should match"); + is(event.data.appVersion, navigator.appVersion, "appVersion should match"); + isnot(event.data.appVersion, "appVersion overridden", "appVersion is not overridden"); + is(event.data.platform, navigator.platform, "platform should match"); + isnot(event.data.platform, "platform overridden", "platform is not overridden"); + is(event.data.userAgent, navigator.userAgent, "userAgent should match"); + is(event.data.product, navigator.product, "product should match"); + runTests(); + }; + } + + function replaceAndCheckValues() { + SpecialPowers.pushPrefEnv({"set": [ + ["general.appversion.override", "appVersion overridden"], + ["general.platform.override", "platform overridden"], + ["general.useragent.override", "userAgent overridden"] + ]}, checkValues); + } + + var tests = [ + replaceAndCheckValues, + checkValues + ]; + + function runTests() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + runTests(); + + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_bug1063538.html b/dom/workers/test/test_bug1063538.html new file mode 100644 index 0000000000..a1dc6624b9 --- /dev/null +++ b/dom/workers/test/test_bug1063538.html @@ -0,0 +1,47 @@ +<!-- +2 Any copyright is dedicated to the Public Domain. +3 http://creativecommons.org/publicdomain/zero/1.0/ +4 --> +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1063538 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1063538</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1063538">Mozilla Bug 1063538</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +function runTest() { + var worker = new Worker("bug1063538_worker.js"); + + worker.onmessage = function(e) { + if (e.data.type == 'finish') { + ok(e.data.progressFired, "Progress was fired."); + SimpleTest.finish(); + } + }; + + worker.postMessage(true); +} + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], runTest); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1104064.html b/dom/workers/test/test_bug1104064.html new file mode 100644 index 0000000000..1c8b3ac92c --- /dev/null +++ b/dom/workers/test/test_bug1104064.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1104064</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +var worker = new Worker("bug1104064_worker.js"); +worker.onmessage = function() { + ok(true, "setInterval has been called twice."); + SimpleTest.finish(); +} +worker.postMessage("go"); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1132395.html b/dom/workers/test/test_bug1132395.html new file mode 100644 index 0000000000..8d424e5ad4 --- /dev/null +++ b/dom/workers/test/test_bug1132395.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for 1132395</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +// This test is full of dummy debug messages. This is because I need to follow +// an hard-to-reproduce timeout failure. + +info("test started"); +var sw = new SharedWorker('bug1132395_sharedWorker.js'); +sw.port.onmessage = function(event) { + info("sw.onmessage received"); + ok(true, "We didn't crash."); + SimpleTest.finish(); +} + +sw.onerror = function(event) { + ok(false, "Failed to create a ServiceWorker"); + SimpleTest.finish(); +} + +info("sw.postmessage called"); +sw.port.postMessage('go'); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1132924.html b/dom/workers/test/test_bug1132924.html new file mode 100644 index 0000000000..b5b952e908 --- /dev/null +++ b/dom/workers/test/test_bug1132924.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for 1132924</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +var w = new Worker('bug1132924_worker.js'); +w.onmessage = function(event) { + ok(true, "We are still alive."); + SimpleTest.finish(); +} + +w.postMessage('go'); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1278777.html b/dom/workers/test/test_bug1278777.html new file mode 100644 index 0000000000..c995212bd0 --- /dev/null +++ b/dom/workers/test/test_bug1278777.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1278777 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1278777</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1278777">Mozilla Bug 1278777</a> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var worker = new Worker('worker_bug1278777.js'); +worker.onerror = function() { + ok(false, "We should not see any error."); + SimpleTest.finish(); +} + +worker.onmessage = function(e) { + ok(e.data, "Everything seems ok."); + SimpleTest.finish(); +} + + </script> +</body> +</html> diff --git a/dom/workers/test/test_bug1301094.html b/dom/workers/test/test_bug1301094.html new file mode 100644 index 0000000000..efe25fa3a9 --- /dev/null +++ b/dom/workers/test/test_bug1301094.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1301094 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1301094</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301094">Mozilla Bug 1301094</a> + <input id="file" type="file"></input> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var url = SimpleTest.getTestFileURL("script_createFile.js"); +script = SpecialPowers.loadChromeScript(url); + +var mainThreadOk, workerOk; + +function maybeFinish() { + if (mainThreadOk & workerOk) { + SimpleTest.finish(); + } +} + +function onOpened(message) { + var input = document.getElementById('file'); + SpecialPowers.wrap(input).mozSetDndFilesAndDirectories([message.data]); + + var worker = new Worker('worker_bug1301094.js'); + worker.onerror = function() { + ok(false, "We should not see any error."); + SimpleTest.finish(); + } + + worker.onmessage = function(e) { + ok(e.data, "Everything seems OK on the worker-side."); + + workerOk = true; + maybeFinish(); + } + + is(input.files.length, 1, "We have something"); + ok(input.files[0] instanceof Blob, "We have one Blob"); + worker.postMessage(input.files[0]); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", 'worker_bug1301094.js', false); + xhr.onload = function() { + ok(xhr.responseText, "Everything seems OK on the main-thread-side."); + mainThreadOk = true; + maybeFinish(); + }; + + var fd = new FormData(); + fd.append('file', input.files[0]); + xhr.send(fd); +} + +script.addMessageListener("file.opened", onOpened); +script.sendAsyncMessage("file.open"); + + </script> +</body> +</html> diff --git a/dom/workers/test/test_bug1317725.html b/dom/workers/test/test_bug1317725.html new file mode 100644 index 0000000000..7da3fc2644 --- /dev/null +++ b/dom/workers/test/test_bug1317725.html @@ -0,0 +1,49 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1317725</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<input type="file" id="file" /> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var url = SimpleTest.getTestFileURL("script_createFile.js"); +script = SpecialPowers.loadChromeScript(url); + +function onOpened(message) { + var input = document.getElementById('file'); + SpecialPowers.wrap(input).mozSetFileArray([message.data]); + + var worker = new Worker("test_bug1317725.js"); + worker.onerror = function(e) { + ok(false, "We should not see any error."); + SimpleTest.finish(); + } + + worker.onmessage = function(e) { + ok(e.data, "Everything seems OK on the worker-side."); + SimpleTest.finish(); + } + + is(input.files.length, 1, "We have something"); + ok(input.files[0] instanceof Blob, "We have one Blob"); + worker.postMessage(input.files[0]); +} + +script.addMessageListener("file.opened", onOpened); +script.sendAsyncMessage("file.open"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1317725.js b/dom/workers/test/test_bug1317725.js new file mode 100644 index 0000000000..858488fac3 --- /dev/null +++ b/dom/workers/test/test_bug1317725.js @@ -0,0 +1,8 @@ +onmessage = function (e) { + var data = new FormData(); + data.append("Filedata", e.data.slice(0, 127), encodeURI(e.data.name)); + var xhr = new XMLHttpRequest(); + xhr.open("POST", location.href, false); + xhr.send(data); + postMessage("No crash \\o/"); +}; diff --git a/dom/workers/test/test_bug1824498.html b/dom/workers/test/test_bug1824498.html new file mode 100644 index 0000000000..ea01cabe1b --- /dev/null +++ b/dom/workers/test/test_bug1824498.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Worker Import failure (Bug 1824498)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1824498"> +Worker Import failure test: Bug 1824498 +</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +document.addEventListener("DOMContentLoaded", async () => { + await SpecialPowers.pushPrefEnv( + { set: [["dom.workers.modules.enabled", true ]] }); + + const worker = new Worker("worker_bug1824498.mjs", {"type": "module"}) + worker.onerror = function(event) { + ok(true, "not assert"); + SimpleTest.finish(); + }; +}); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug949946.html b/dom/workers/test/test_bug949946.html new file mode 100644 index 0000000000..41b021a098 --- /dev/null +++ b/dom/workers/test/test_bug949946.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 949946</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +new SharedWorker('sharedWorker_sharedWorker.js'); +new SharedWorker('sharedWorker_sharedWorker.js', ':'); +new SharedWorker('sharedWorker_sharedWorker.js', '|||'); +ok(true, "3 SharedWorkers created!"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug978260.html b/dom/workers/test/test_bug978260.html new file mode 100644 index 0000000000..056b8b6c72 --- /dev/null +++ b/dom/workers/test/test_bug978260.html @@ -0,0 +1,35 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + SimpleTest.waitForExplicitFinish(); + + var xhr = new XMLHttpRequest(); + xhr.onload = function () { + var worker = new Worker("bug978260_worker.js"); + worker.onmessage = function(event) { + is(event.data, "loaded"); + SimpleTest.finish(); + } + } + + xhr.open('GET', '/', false); + xhr.send(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug998474.html b/dom/workers/test/test_bug998474.html new file mode 100644 index 0000000000..93632a5de4 --- /dev/null +++ b/dom/workers/test/test_bug998474.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 998474</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="boom();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +function boom() +{ + var worker = new SharedWorker("bug998474_worker.js"); + + setTimeout(function() { + port = worker.port; + port.postMessage(""); + + setTimeout(function() { + port.start(); + ok(true, "Still alive!"); + SimpleTest.finish(); + }, 150); + }, 150); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_chromeWorker.html b/dom/workers/test/test_chromeWorker.html new file mode 100644 index 0000000000..35d5c08928 --- /dev/null +++ b/dom/workers/test/test_chromeWorker.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + try { + var worker = new ChromeWorker("simpleThread_worker.js"); + ok(false, "ChromeWorker constructor should be blocked!"); + } + catch (e) { + ok(true, "ChromeWorker constructor wasn't blocked!"); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_chromeWorker.xhtml b/dom/workers/test/test_chromeWorker.xhtml new file mode 100644 index 0000000000..65d14f8851 --- /dev/null +++ b/dom/workers/test/test_chromeWorker.xhtml @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + add_task(async function classic_worker_test() { + let worker = window.classicWorker = new ChromeWorker("chromeWorker_worker.js"); + await new Promise((resolve, reject) => { + worker.onmessage = function(event) { + is(event.data, "Done!", "Got the done message!"); + resolve(); + }; + worker.onerror = function(event) { + ok(false, "Classic Worker had an error: " + event.message); + worker.terminate(); + reject(); + }; + worker.postMessage("go"); + }); + }); + + add_task(async function module_worker_test() { + waitForWorkerFinish(); + + let worker = window.moduleWorker = new ChromeWorker("chromeWorker_worker.sys.mjs", { type: "module" }); + await new Promise((resolve, reject) => { + worker.onmessage = function(event) { + is(event.data, "Done!", "Got the done message!"); + resolve(); + }; + worker.onerror = function(event) { + ok(false, "Module Worker had an error: " + event.message); + worker.terminate(); + reject(); + }; + worker.postMessage("go"); + }); + }); + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_chromeWorkerJSM.xhtml b/dom/workers/test/test_chromeWorkerJSM.xhtml new file mode 100644 index 0000000000..6341737815 --- /dev/null +++ b/dom/workers/test/test_chromeWorkerJSM.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + function test() + { + waitForWorkerFinish(); + + var worker; + + function done() + { + worker = null; + finish(); + } + + function messageCallback(event) { + is(event.data, "Done", "Correct message"); + done(); + } + + function errorCallback(event) { + ok(false, "Worker had an error: " + event.message); + done(); + } + + const {WorkerTest} = ChromeUtils.import("chrome://mochitests/content/chrome/dom/workers/test/WorkerTest.jsm"); + + worker = WorkerTest.go(window.location.href, messageCallback, + errorCallback); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_clearTimeouts.html b/dom/workers/test/test_clearTimeouts.html new file mode 100644 index 0000000000..caa87fbf56 --- /dev/null +++ b/dom/workers/test/test_clearTimeouts.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + new Worker("clearTimeouts_worker.js").onmessage = function(event) { + event.target.terminate(); + + is(event.data, "ready", "Correct message"); + setTimeout(function() { SimpleTest.finish(); }, 1000); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_clearTimeoutsImplicit.html b/dom/workers/test/test_clearTimeoutsImplicit.html new file mode 100644 index 0000000000..59a37974ca --- /dev/null +++ b/dom/workers/test/test_clearTimeoutsImplicit.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + new Worker("clearTimeoutsImplicit_worker.js").onmessage = function(event) { + event.target.terminate(); + + is(event.data, "ready", "Correct message"); + setTimeout(function() { SimpleTest.finish(); }, 1000); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_console.html b/dom/workers/test/test_console.html new file mode 100644 index 0000000000..b8c0f189ab --- /dev/null +++ b/dom/workers/test/test_console.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Console +--> +<head> + <title>Test for DOM Worker Console</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + var worker = new Worker("console_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "Worker and target match!"); + ok(event.data.status, event.data.event); + + if (!event.data.status || event.data.last) + SimpleTest.finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + worker.postMessage(true); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_consoleAndBlobs.html b/dom/workers/test/test_consoleAndBlobs.html new file mode 100644 index 0000000000..e07cfe5dca --- /dev/null +++ b/dom/workers/test/test_consoleAndBlobs.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for console API and blobs</title> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + </head> + <body> + <script type="text/javascript"> + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + + function consoleListener() { + this.observe = this.observe.bind(this); + ConsoleAPIStorage.addLogEventListener(this.observe, SpecialPowers.wrap(document).nodePrincipal); + } + + var order = 0; + consoleListener.prototype = { + observe(aSubject) { + ok(true, "Something has been received"); + + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] && obj.arguments[0].msg === 'consoleAndBlobs') { + ConsoleAPIStorage.removeLogEventListener(this.observe); + is(obj.arguments[0].blob.size, 3, "The size is correct"); + is(obj.arguments[0].blob.type, 'foo/bar', "The type is correct"); + SimpleTest.finish(); + } + } + } + + var cl = new consoleListener(); + + new Worker('worker_consoleAndBlobs.js'); + SimpleTest.waitForExplicitFinish(); + + </script> + </body> +</html> diff --git a/dom/workers/test/test_consoleReplaceable.html b/dom/workers/test/test_consoleReplaceable.html new file mode 100644 index 0000000000..b8a60411e4 --- /dev/null +++ b/dom/workers/test/test_consoleReplaceable.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Console +--> +<head> + <title>Test for DOM Worker Console</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + var worker = new Worker("consoleReplaceable_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "Worker and target match!"); + ok(event.data.status, event.data.event); + + if (event.data.last) + SimpleTest.finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + worker.postMessage(true); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_contentWorker.html b/dom/workers/test/test_contentWorker.html new file mode 100644 index 0000000000..38891a88c5 --- /dev/null +++ b/dom/workers/test/test_contentWorker.html @@ -0,0 +1,48 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker privileged properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var workerFilename = "content_worker.js"; + var worker = new Worker(workerFilename); + + var props = { + 'ctypes': 1, + 'OS': 1 + }; + + worker.onmessage = function(event) { + if (event.data.testfinished) { + SimpleTest.finish(); + return; + } + var prop = event.data.prop; + ok(prop in props, "checking " + prop); + is(event.data.value, undefined, prop + " should be undefined"); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_csp.html b/dom/workers/test/test_csp.html new file mode 100644 index 0000000000..f3ef747372 --- /dev/null +++ b/dom/workers/test/test_csp.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker + CSP</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +<script type="text/javascript" src="test_csp.js"></script> +</html> diff --git a/dom/workers/test/test_csp.html^headers^ b/dom/workers/test/test_csp.html^headers^ new file mode 100644 index 0000000000..1c93210799 --- /dev/null +++ b/dom/workers/test/test_csp.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Content-Security-Policy: default-src 'self' blob: diff --git a/dom/workers/test/test_csp.js b/dom/workers/test/test_csp.js new file mode 100644 index 0000000000..8c2b53586a --- /dev/null +++ b/dom/workers/test/test_csp.js @@ -0,0 +1,54 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var tests = 3; + +SimpleTest.waitForExplicitFinish(); + +testDone = function (event) { + if (!--tests) { + SimpleTest.finish(); + } +}; + +// Workers don't inherit CSP +worker = new Worker("csp_worker.js"); +worker.postMessage({ do: "eval" }); +worker.onmessage = function (event) { + is(event.data, 42, "Eval succeeded!"); + testDone(); +}; + +// blob: workers *do* inherit CSP +xhr = new XMLHttpRequest(); +xhr.open("GET", "csp_worker.js"); +xhr.responseType = "blob"; +xhr.send(); +xhr.onload = e => { + uri = URL.createObjectURL(e.target.response); + worker = new Worker(uri); + worker.postMessage({ do: "eval" }); + worker.onmessage = function (event) { + is(event.data, "EvalError: call to eval() blocked by CSP", "Eval threw"); + testDone(); + }; +}; + +xhr = new XMLHttpRequest(); +xhr.open("GET", "csp_worker.js"); +xhr.responseType = "blob"; +xhr.send(); +xhr.onload = e => { + uri = URL.createObjectURL(e.target.response); + worker = new Worker(uri); + worker.postMessage({ do: "nest", uri, level: 3 }); + worker.onmessage = function (event) { + is( + event.data, + "EvalError: call to eval() blocked by CSP", + "Eval threw in nested worker" + ); + testDone(); + }; +}; diff --git a/dom/workers/test/test_dataURLWorker.html b/dom/workers/test/test_dataURLWorker.html new file mode 100644 index 0000000000..145b0e43f1 --- /dev/null +++ b/dom/workers/test/test_dataURLWorker.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const message = "hi"; + const url = "DATA:text/plain," + + "onmessage = function(event) {" + + " postMessage(event.data);" + + "};"; + + var worker = new Worker(url); + worker.onmessage = function(event) { + is(event.data, message, "Got correct message"); + SimpleTest.finish(); + }; + worker.postMessage(message); + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_dynamicImport.html b/dom/workers/test/test_dynamicImport.html new file mode 100644 index 0000000000..bffef87d2d --- /dev/null +++ b/dom/workers/test/test_dynamicImport.html @@ -0,0 +1,76 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of Worker Dynamic Import (Bug 1540913) +Ensure that the script loader doesn't accidentally reorder events due to async work +done by dynamic import +--> +<head> + <title>Test for Worker Dynamic Import (Bug 1540913)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="onLoad()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1540913">Worker Dynamic Import + Bug 1540913</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +async function onLoad() { + await SpecialPowers.pushPrefEnv( + { set: [["dom.workers.modules.enabled", true ]] }); + + const workers = [ + new Worker("dynamicImport_worker.js", {type: "classic"}), + new Worker("dynamicImport_worker.js", {type: "module"}) + ]; + + let successCount = 0; + + for (const worker of workers) { + const events = []; + worker.onmessage = function(event) { + switch (event.data) { + case "first": + ok(events.length === 1 && events[0] === "second", + "first dynamic import returned"); + events.push(event.data); + successCount++; + // Cheap way to make sure we only finish successfully after + // both the module and classic test is finished. + if (successCount == 2) { + SimpleTest.finish(); + } + break; + case "second": + ok(events.length === 0, + "second dynamic import returned"); + events.push(event.data); + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error:" + event.message); + SimpleTest.finish(); + } + + worker.postMessage("start"); + } +} +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_dynamicImport_and_terminate.html b/dom/workers/test/test_dynamicImport_and_terminate.html new file mode 100644 index 0000000000..cf355572e6 --- /dev/null +++ b/dom/workers/test/test_dynamicImport_and_terminate.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Worker create script loader failure</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +document.addEventListener("DOMContentLoaded", () => { + const worker = new Worker("worker_dynamicImport.mjs", {"type": "module"}); + setTimeout(() => { + worker.terminate(); + ok(true, "done"); + SimpleTest.finish(); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_dynamicImport_early_termination.html b/dom/workers/test/test_dynamicImport_early_termination.html new file mode 100644 index 0000000000..fb9096df14 --- /dev/null +++ b/dom/workers/test/test_dynamicImport_early_termination.html @@ -0,0 +1,79 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of Worker Dynamic Import (Bug 1540913) +Ensure that the script loader doesn't fail if requests are terminated early. +--> +<head> + <title>Test for Worker Dynamic Import (Bug 1540913)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="onLoad()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1540913">Worker Dynamic Import + Bug 1540913</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +async function onLoad() { + await SpecialPowers.pushPrefEnv( + { set: [["dom.workers.modules.enabled", true ]] }); + + const workers = [ + new Worker("dynamicImport_worker.js", {type: "classic"}), + new Worker("dynamicImport_worker.js", {type: "module"}) + ] + + let successCount = 0; + + // In the implementation of dynamic import, every dynamic import has + // it's own ScriptLoader. To ensure that this is working correctly, + // this tests that if we re-order the dynamic import order, + // worker termination works as expected. + for (const worker of workers) { + const events = []; + worker.onmessage = function(event) { + switch (event.data) { + case "first": + ok(false, "first dynamic import returned"); + SimpleTest.finish(); + break; + case "second": + ok(events.length === 0, + "second dynamic import returned"); + events.push(event.data); + worker.terminate() + successCount++; + // Cheap way to make sure we only finish successfully after + // both the module and classic test is finished. + if (successCount == 2) { + SimpleTest.finish(); + } + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error:" + event.message); + SimpleTest.finish(); + } + + worker.postMessage("start"); + } +} +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_errorPropagation.html b/dom/workers/test/test_errorPropagation.html new file mode 100644 index 0000000000..8eb899fe7e --- /dev/null +++ b/dom/workers/test/test_errorPropagation.html @@ -0,0 +1,65 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <iframe id="workerFrame" src="errorPropagation_iframe.html" + onload="workerFrameLoaded();"></iframe> + <script type="text/javascript"> + const workerCount = 3; + + const errorMessage = "Error: expectedError"; + const errorFilename = "http://mochi.test:8888/tests/dom/workers/test/" + + "errorPropagation_worker.js"; + const errorLineno = 49; + + var workerFrame; + + scopeErrorCount = 0; + workerErrorCount = 0; + windowErrorCount = 0; + + function messageListener(event) { + if (event.type == "scope") { + scopeErrorCount++; + } + else if (event.type == "worker") { + workerErrorCount++; + } + else if (event.type == "window") { + windowErrorCount++; + } + else { + ok(false, "Bad event type: " + event.type); + } + + is(event.data.message, errorMessage, "Correct message event.message"); + is(event.data.filename, errorFilename, + "Correct message event.filename"); + is(event.data.lineno, errorLineno, "Correct message event.lineno"); + + if (windowErrorCount == 1) { + is(scopeErrorCount, workerCount, "Good number of scope errors"); + is(workerErrorCount, workerCount, "Good number of worker errors"); + workerFrame.stop(); + SimpleTest.finish(); + } + } + + function workerFrameLoaded() { + workerFrame = document.getElementById("workerFrame").contentWindow; + workerFrame.start(workerCount, messageListener); + } + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_errorwarning.html b/dom/workers/test/test_errorwarning.html new file mode 100644 index 0000000000..282b46ec20 --- /dev/null +++ b/dom/workers/test/test_errorwarning.html @@ -0,0 +1,93 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Test javascript.options.strict in Workers +--> +<head> + <title>Test javascript.options.strict in Workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var errors = 0; + function errorHandler(e) { + ok(true, "An error has been received!"); + errors++; + } + + function test_noErrors() { + errors = 0; + + var worker = new Worker('errorwarning_worker.js'); + worker.onerror = errorHandler; + worker.onmessage = function(e) { + if (e.data.type == 'ignore') + return; + + if (e.data.type == 'error') { + errorHandler(); + return; + } + + if (e.data.type == 'finish') { + ok(errors == 0, "Here we are with 0 errors!"); + runTests(); + } + } + + onerror = errorHandler; + worker.postMessage({ loop: 5, errors: false }); + } + + function test_errors() { + errors = 0; + + var worker = new Worker('errorwarning_worker.js'); + worker.onerror = errorHandler; + worker.onmessage = function(e) { + if (e.data.type == 'ignore') + return; + + if (e.data.type == 'error') { + errorHandler(); + return; + } + + if (e.data.type == 'finish') { + ok(errors != 0, "Here we are with errors!"); + runTests(); + } + } + + onerror = errorHandler; + worker.postMessage({ loop: 5, errors: true }); + } + + var tests = [ test_noErrors, test_errors ]; + function runTests() { + var test = tests.shift(); + if (test) { + test(); + } else { + SimpleTest.finish(); + } + } + + runTests(); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_eventDispatch.html b/dom/workers/test/test_eventDispatch.html new file mode 100644 index 0000000000..e6bbd7e2d1 --- /dev/null +++ b/dom/workers/test/test_eventDispatch.html @@ -0,0 +1,32 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const message = "Hi"; + + var messageCount = 0; + + var worker = new Worker("eventDispatch_worker.js"); + worker.onmessage = function(event) { + is(event.data, message, "Got correct data."); + if (!messageCount++) { + event.target.postMessage(event.data); + return; + } + SimpleTest.finish(); + } + worker.postMessage(message); + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_fibonacci.html b/dom/workers/test/test_fibonacci.html new file mode 100644 index 0000000000..c3e3e98574 --- /dev/null +++ b/dom/workers/test/test_fibonacci.html @@ -0,0 +1,51 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads with Fibonacci +--> +<head> + <title>Test for DOM Worker Threads with Fibonacci</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads Fibonacci</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + const seqNum = 5; + + function recursivefib(n) { + return n < 2 ? n : recursivefib(n - 1) + recursivefib(n - 2); + } + + var worker = new Worker("fibonacci_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker); + is(event.data, recursivefib(seqNum)); + SimpleTest.finish(); + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + }; + + worker.postMessage(seqNum); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_file.xhtml b/dom/workers/test/test_file.xhtml new file mode 100644 index 0000000000..2b628e7f4d --- /dev/null +++ b/dom/workers/test/test_file.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=123456 +--> +<window title="Mozilla Bug 123456" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=123456" + target="_blank">Mozilla Bug 123456</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 123456 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerFile" + fileNum++ + fileExtension); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker to access file properties. + */ + function accessFileProperties(file, expectedSize, expectedType) { + waitForWorkerFinish(); + + var worker = new Worker("file_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data.size, expectedSize, "size proproperty accessed from worker is not the same as on main thread."); + is(event.data.type, expectedType, "type proproperty accessed from worker is incorrect."); + is(event.data.name, file.name, "name proproperty accessed from worker is incorrect."); + is(event.data.lastModified, file.lastModified, "lastModified proproperty accessed from worker is incorrect."); + finish(); + }; + + worker.postMessage(file); + } + + // Empty file. + accessFileProperties(createFileWithData(""), 0, ""); + + // Typical use case. + accessFileProperties(createFileWithData("Hello"), 5, ""); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + accessFileProperties(createFileWithData(text), 40000, ""); + + // Type detection based on extension. + accessFileProperties(createFileWithData("text", "txt"), 4, "text/plain"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileBlobPosting.xhtml b/dom/workers/test/test_fileBlobPosting.xhtml new file mode 100644 index 0000000000..61f4a8e909 --- /dev/null +++ b/dom/workers/test/test_fileBlobPosting.xhtml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("workerBlobPosting" + fileNum++); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker which posts the same blob given. Used to test cloning of blobs. + * Checks the size, type, name and path of the file posted from the worker to ensure it + * is the same as the original. + */ + function postBlob(file) { + var worker = new Worker("filePosting_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + console.log(event.data); + is(event.data.size, file.size, "size of file posted from worker does not match file posted to worker."); + finish(); + }; + + var blob = file.slice(); + worker.postMessage(blob); + waitForWorkerFinish(); + } + + // Empty file. + postBlob(createFileWithData("")); + + // Typical use case. + postBlob(createFileWithData("Hello")); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileBlobSubWorker.xhtml b/dom/workers/test/test_fileBlobSubWorker.xhtml new file mode 100644 index 0000000000..8b67552788 --- /dev/null +++ b/dom/workers/test/test_fileBlobSubWorker.xhtml @@ -0,0 +1,97 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerBlobSubWorker" + fileNum++ + fileExtension); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker to access blob properties. + */ + function accessFileProperties(file, expectedSize) { + var worker = new Worker("fileBlobSubWorker_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + if (event.data == undefined) { + ok(false, "Worker had an error."); + } else { + is(event.data.size, expectedSize, "size proproperty accessed from worker is not the same as on main thread."); + } + finish(); + }; + + var blob = file.slice(); + worker.postMessage(blob); + waitForWorkerFinish(); + } + + // Empty file. + accessFileProperties(createFileWithData(""), 0); + + // Typical use case. + accessFileProperties(createFileWithData("Hello"), 5); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + accessFileProperties(createFileWithData(text), 40000); + + // Type detection based on extension. + accessFileProperties(createFileWithData("text", "txt"), 4); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_filePosting.xhtml b/dom/workers/test/test_filePosting.xhtml new file mode 100644 index 0000000000..3ffa219516 --- /dev/null +++ b/dom/workers/test/test_filePosting.xhtml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("workerFilePosting" + fileNum++); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker which posts the same file given. Used to test cloning of files. + * Checks the size, type, name and path of the file posted from the worker to ensure it + * is the same as the original. + */ + function postFile(file) { + var worker = new Worker("file_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data.size, file.size, "size of file posted from worker does not match file posted to worker."); + is(event.data.type, file.type, "type of file posted from worker does not match file posted to worker."); + is(event.data.name, file.name, "name of file posted from worker does not match file posted to worker."); + finish(); + }; + + worker.postMessage(file); + waitForWorkerFinish(); + } + + // Empty file. + postFile(createFileWithData("")); + + // Typical use case. + postFile(createFileWithData("Hello")); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileReadSlice.xhtml b/dom/workers/test/test_fileReadSlice.xhtml new file mode 100644 index 0000000000..fa396e88e8 --- /dev/null +++ b/dom/workers/test/test_fileReadSlice.xhtml @@ -0,0 +1,93 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + if (navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); + } + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("workerReadSlice" + fileNum++); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Creates a worker which slices a blob to the given start and end offset and + * reads the content as text. + */ + function readSlice(blob, start, end, expectedText) { + var worker = new Worker("fileReadSlice_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data, expectedText, "Text from sliced blob in worker is incorrect."); + finish(); + }; + + var params = {blob, start, end}; + worker.postMessage(params); + waitForWorkerFinish(); + } + + // Empty file. + readSlice(createFileWithData(""), 0, 0, ""); + + // Typical use case. + readSlice(createFileWithData("HelloBye"), 5, 8, "Bye"); + + // End offset too large. + readSlice(createFileWithData("HelloBye"), 5, 9, "Bye"); + + // Start of file. + readSlice(createFileWithData("HelloBye"), 0, 5, "Hello"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileReaderSync.xhtml b/dom/workers/test/test_fileReaderSync.xhtml new file mode 100644 index 0000000000..bba2890b5e --- /dev/null +++ b/dom/workers/test/test_fileReaderSync.xhtml @@ -0,0 +1,198 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerFileReaderSync" + fileNum++ + fileExtension); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + function convertToUTF16(s) { + res = ""; + for (var i = 0; i < s.length; ++i) { + c = s.charCodeAt(i); + res += String.fromCharCode(c & 255, c >>> 8); + } + return res; + } + + /** + * Converts the given string to a data URL of the specified mime type. + */ + function convertToDataURL(mime, s) { + return "data:" + mime + ";base64," + btoa(s); + } + + /** + * Create a worker to read a file containing fileData using FileReaderSync and + * checks the return type against the expected type. Optionally set an encoding + * for reading the file as text. + */ + function readFileData(fileData, expectedText, /** optional */ encoding) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onmessage = function(event) { + is(event.data.text, expectedText, "readAsText in worker returned incorrect result."); + is(event.data.bin, fileData, "readAsBinaryString in worker returned incorrect result."); + is(event.data.url, convertToDataURL("application/octet-stream", fileData), "readAsDataURL in worker returned incorrect result."); + is(event.data.arrayBuffer.byteLength, fileData.length, "readAsArrayBuffer returned buffer of incorrect length."); + finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var params = {file: createFileWithData(fileData), encoding}; + + worker.postMessage(params); + + waitForWorkerFinish(); + } + + /** + * Create a worker which reuses a FileReaderSync to read multiple files as DataURLs. + */ + function reuseReaderForURL(files, expected) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var k = 0; + worker.onmessage = function(event) { + is(event.data.url, expected[k], "readAsDataURL in worker returned incorrect result when reusing FileReaderSync."); + k++; + finish(); + }; + + for (var i = 0; i < files.length; ++i) { + var params = {file: files[i], encoding: undefined}; + worker.postMessage(params); + waitForWorkerFinish(); + } + } + + /** + * Create a worker which reuses a FileReaderSync to read multiple files as text. + */ + function reuseReaderForText(fileData, expected) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var k = 0; + worker.onmessage = function(event) { + is(event.data.text, expected[k++], "readAsText in worker returned incorrect result when reusing FileReaderSync."); + finish(); + }; + + for (var i = 0; i < fileData.length; ++i) { + var params = {file: createFileWithData(fileData[i]), encoding: undefined}; + worker.postMessage(params); + waitForWorkerFinish(); + } + } + + + /** + * Creates a a worker which reads a file containing fileData as an ArrayBuffer. + * Verifies that the ArrayBuffer when interpreted as a string matches the original data. + */ + function readArrayBuffer(fileData) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onmessage = function(event) { + var view = new Uint8Array(event.data.arrayBuffer); + is(event.data.arrayBuffer.byteLength, fileData.length, "readAsArrayBuffer returned buffer of incorrect length."); + is(String.fromCharCode.apply(String, view), fileData, "readAsArrayBuffer returned buffer containing incorrect data."); + finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var params = {file: createFileWithData(fileData), encoding: undefined}; + + worker.postMessage(params); + + waitForWorkerFinish(); + } + + // Empty file. + readFileData("", ""); + + // Typical use case. + readFileData("text", "text"); + + // Test reading UTF-16 characters. + readFileData(convertToUTF16("text"), "text", "UTF-16"); + + // First read a file of type "text/plain", then read a file of type "application/octet-stream". + reuseReaderForURL([createFileWithData("text", "txt"), createFileWithData("text")], + [convertToDataURL("text/plain", "text"), + convertToDataURL("application/octet-stream", "text")]); + + // First read UTF-16 characters marked using BOM, then read UTF-8 characters. + reuseReaderForText([convertToUTF16("\ufefftext"), "text"], + ["text", "text"]); + + // Reading data as ArrayBuffer. + readArrayBuffer(""); + readArrayBuffer("text"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileReaderSyncErrors.xhtml b/dom/workers/test/test_fileReaderSyncErrors.xhtml new file mode 100644 index 0000000000..626b67e1ba --- /dev/null +++ b/dom/workers/test/test_fileReaderSyncErrors.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("workerFileReaderSyncErrors" + fileNum++); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Creates a worker which runs errors cases. + */ + function runWorkerErrors(file) { + var worker = new Worker("fileReaderSyncErrors_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + if(event.data == undefined) { + // Worker returns undefined when tests have finished running. + finish(); + } else { + // Otherwise worker will return results of tests to be evaluated. + is(event.data.actual, event.data.expected, event.data.message); + } + }; + + worker.postMessage(file); + waitForWorkerFinish(); + } + + // Run worker which creates exceptions. + runWorkerErrors(createFileWithData("text")); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileReaderSync_when_closing.html b/dom/workers/test/test_fileReaderSync_when_closing.html new file mode 100644 index 0000000000..d5fadbaa65 --- /dev/null +++ b/dom/workers/test/test_fileReaderSync_when_closing.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for FileReaderSync when the worker is closing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +// In order to exercise FileReaderSync::SyncRead's syncLoop-using AsyncWait() +// path, we need to provide a stream that will both 1) not have all the data +// immediately available (eliminating memory-backed Blobs) and 2) return +// NS_BASE_STREAM_WOULD_BLOCK. Under e10s, any Blob/File sourced from the +// parent process (as loadChromeScript performs) will be backed by an +// RemoteLazyInputStream and will behave this way on first use (when it is in +// the eInit state). For ease of testing, we reuse script_createFile.js which +// involves a file on disk, but a memory-backed Blob from the parent process +// would be equally fine. Under non-e10s, this File will not do the right +// thing because a synchronous nsFileInputStream will be made directly +// available and the AsyncWait path won't be taken, but the test will still +// pass. + +var url = SimpleTest.getTestFileURL("script_createFile.js"); +var script = SpecialPowers.loadChromeScript(url); + +function onOpened(message) { + function workerCode() { + onmessage = function(e) { + self.close(); + var fr = new FileReaderSync(); + self.postMessage(fr.readAsText(e.data)); + } + } + + var b = new Blob([workerCode+'workerCode();']); + var w = new Worker(URL.createObjectURL(b)); + w.onmessage = function(e) { + is(e.data, "Hello world!", "The blob content is OK!"); + SimpleTest.finish(); + } + + w.postMessage(message.data); +} + +script.addMessageListener("nonEmptyFile.opened", onOpened); +script.sendAsyncMessage("nonEmptyFile.open"); + +SimpleTest.waitForExplicitFinish(); + + </script> +</body> +</html> diff --git a/dom/workers/test/test_fileSlice.xhtml b/dom/workers/test/test_fileSlice.xhtml new file mode 100644 index 0000000000..c352404cc1 --- /dev/null +++ b/dom/workers/test/test_fileSlice.xhtml @@ -0,0 +1,105 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerSlice" + fileNum++ + fileExtension); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Starts a worker which slices the blob to the given start offset and optional end offset and + * content type. It then verifies that the size and type of the sliced blob is correct. + */ + function createSlice(blob, start, expectedLength, /** optional */ end, /** optional */ contentType) { + var worker = new Worker("fileSlice_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data.size, expectedLength, "size property of slice is incorrect."); + is(event.data.type, contentType ? contentType : blob.type, "type property of slice is incorrect."); + finish(); + }; + + var params = {blob, start, end, contentType}; + worker.postMessage(params); + waitForWorkerFinish(); + } + + // Empty file. + createSlice(createFileWithData(""), 0, 0, 0); + + // Typical use case. + createSlice(createFileWithData("Hello"), 1, 1, 2); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + createSlice(createFileWithData(text), 2000, 2000, 4000); + + // Slice to different type. + createSlice(createFileWithData("text", "txt"), 0, 2, 2, "image/png"); + + // Length longer than blob. + createSlice(createFileWithData("text"), 0, 4, 20); + + // Start longer than blob. + createSlice(createFileWithData("text"), 20, 0, 4); + + // No optional arguments + createSlice(createFileWithData("text"), 0, 4); + createSlice(createFileWithData("text"), 2, 2); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileSubWorker.xhtml b/dom/workers/test/test_fileSubWorker.xhtml new file mode 100644 index 0000000000..92aa3b1a17 --- /dev/null +++ b/dom/workers/test/test_fileSubWorker.xhtml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerSubWorker" + fileNum++ + fileExtension); + + var outStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker to access file properties. + */ + function accessFileProperties(file, expectedSize, expectedType) { + var worker = new Worker("fileSubWorker_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + if (event.data == undefined) { + ok(false, "Worker had an error."); + } else { + is(event.data.size, expectedSize, "size proproperty accessed from worker is not the same as on main thread."); + is(event.data.type, expectedType, "type proproperty accessed from worker is incorrect."); + is(event.data.name, file.name, "name proproperty accessed from worker is incorrect."); + } + finish(); + }; + + worker.postMessage(file); + waitForWorkerFinish(); + } + + // Empty file. + accessFileProperties(createFileWithData(""), 0, ""); + + // Typical use case. + accessFileProperties(createFileWithData("Hello"), 5, ""); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + accessFileProperties(createFileWithData(text), 40000, ""); + + // Type detection based on extension. + accessFileProperties(createFileWithData("text", "txt"), 4, "text/plain"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_importScripts.html b/dom/workers/test/test_importScripts.html new file mode 100644 index 0000000000..0e6a3dde5e --- /dev/null +++ b/dom/workers/test/test_importScripts.html @@ -0,0 +1,53 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("importScripts_worker.js"); + + worker.onmessage = function(event) { + switch (event.data) { + case "started": + worker.postMessage("stop"); + break; + case "stopped": + ok(true, "worker correctly stopped"); + SimpleTest.finish(); + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error:" + event.message); + SimpleTest.finish(); + } + + worker.postMessage("start"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_importScripts_1.html b/dom/workers/test/test_importScripts_1.html new file mode 100644 index 0000000000..2c75a3ed91 --- /dev/null +++ b/dom/workers/test/test_importScripts_1.html @@ -0,0 +1,35 @@ +<head> + <title>Test for ServiceWorker importScripts</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script id="worker" type="javascript/worker"> +onconnect = async function(e) { + e.ports[0].onmessage = async function(msg) { + try { + self.importScripts("N:", ""); + } catch (ex) { + e.source.postMessage("done"); + } + }; +}; +</script> +<script> +SimpleTest.waitForExplicitFinish(); +document.addEventListener("DOMContentLoaded", async () => { + const blob = new Blob([document.querySelector('#worker').textContent], + {type: "text/javascript"}); + const sw = new SharedWorker(window.URL.createObjectURL(blob)); + sw.port.postMessage([], []); + sw.port.onmessage = function(e) { + if (e.data == "done") { + ok(true); + SimpleTest.finish(); + } + }; +}); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_importScripts_2.html b/dom/workers/test/test_importScripts_2.html new file mode 100644 index 0000000000..f40b9a64ed --- /dev/null +++ b/dom/workers/test/test_importScripts_2.html @@ -0,0 +1,34 @@ +<head> + <title>Test for Worker importScripts</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script id="worker" type="javascript/worker"> +onmessage = async function(msg) { + try { + self.importScripts("N:", ""); + } catch (ex) { + postMessage("done"); + } +}; + +</script> +<script> +SimpleTest.waitForExplicitFinish(); +document.addEventListener("DOMContentLoaded", async () => { + const blob = new Blob([document.querySelector('#worker').textContent], + {type: "text/javascript"}); + const worker = new Worker(window.URL.createObjectURL(blob)); + worker.postMessage([], []); + worker.onmessage = function(e) { + if (e.data == "done") { + ok(true); + SimpleTest.finish(); + } + }; +}); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_importScripts_3rdparty.html b/dom/workers/test/test_importScripts_3rdparty.html new file mode 100644 index 0000000000..7f10f23faf --- /dev/null +++ b/dom/workers/test/test_importScripts_3rdparty.html @@ -0,0 +1,136 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for 3rd party imported script and muted errors</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +const workerURL = 'http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js'; + +const sameOriginURL = 'http://mochi.test:8888/tests/dom/workers/test/invalid.js' + +var tests = [ + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data!"); + next(); + }; + + worker.postMessage({ url: sameOriginURL, test: 'try', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers!"); + next(); + }; + + worker.postMessage({ url: sameOriginURL, test: 'try', nested: true }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data via eventListener!"); + next(); + }; + + worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers via eventListener!"); + next(); + }; + + worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: true }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data via onerror!"); + next(); + }; + worker.onerror = function(event) { + event.preventDefault(); + } + + worker.postMessage({ url: sameOriginURL, test: 'onerror', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onerror = function(event) { + event.preventDefault(); + ok(event instanceof ErrorEvent, "ErrorEvent received."); + is(event.filename, workerURL, "ErrorEvent.filename is correct"); + next(); + }; + + worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.addEventListener("error", function(event) { + event.preventDefault(); + ok(event instanceof ErrorEvent, "ErrorEvent received."); + is(event.filename, workerURL, "ErrorEvent.filename is correct"); + next(); + }); + + worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onerror = function(event) { + ok(false, "No error should be received!"); + }; + + worker.onmessage = function(event) { + ok("error" in event.data && event.data.error, "The error has been fully received from a nested worker"); + next(); + }; + worker.postMessage({ url: sameOriginURL, test: 'none', nested: true }); + }, + + function() { + var url = URL.createObjectURL(new Blob(["%&%^&%^"])); + var worker = new Worker(url); + worker.onerror = function(event) { + event.preventDefault(); + ok(event instanceof Event, "Event received."); + next(); + }; + } +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); + +</script> +</body> +</html> diff --git a/dom/workers/test/test_importScripts_mixedcontent.html b/dom/workers/test/test_importScripts_mixedcontent.html new file mode 100644 index 0000000000..04281deaea --- /dev/null +++ b/dom/workers/test/test_importScripts_mixedcontent.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1198078 - test that we respect mixed content blocking in importScript() inside workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1198078">DOM Worker Threads Bug 1198078</a> +<iframe></iframe> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + onmessage = function(event) { + switch (event.data.status) { + case "done": + SimpleTest.finish(); + break; + case "ok": + ok(event.data.data, event.data.msg); + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }; + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.workers.sharedWorkers.enabled", true], + ["security.mixed_content.block_active_content", false], + ]}, function() { + var iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/importScripts_mixedcontent.html"; + }); + }; + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_instanceof.html b/dom/workers/test/test_instanceof.html new file mode 100644 index 0000000000..3a66674877 --- /dev/null +++ b/dom/workers/test/test_instanceof.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker JSON messages +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script src="json_worker.js" language="javascript"></script> +<script class="testbody" language="javascript"> + + var worker = new Worker("instanceof_worker.js"); + + worker.onmessage = function(event) { + ok(event.data.status, event.data.event); + + if (event.data.last) + SimpleTest.finish(); + }; + + worker.postMessage(42); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_json.html b/dom/workers/test/test_json.html new file mode 100644 index 0000000000..26e951aa63 --- /dev/null +++ b/dom/workers/test/test_json.html @@ -0,0 +1,89 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker JSON messages +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script src="json_worker.js" language="javascript"></script> +<script class="testbody" language="javascript"> + + ok(messages.length, "No messages to test!"); + + var worker = new Worker("json_worker.js"); + + var index = 0; + worker.onmessage = function(event) { + var key = messages[index++]; + + // Loop for the ones we shouldn't receive. + while (key.exception) { + key = messages[index++]; + } + + is(typeof event.data, key.type, "Bad type! " + messages.indexOf(key)); + + if (key.array) { + is(event.data instanceof Array, key.array, + "Array mismatch! " + messages.indexOf(key)); + } + + if (key.isNaN) { + ok(isNaN(event.data), "Should be NaN!" + messages.indexOf(key)); + } + + if (key.isInfinity) { + is(event.data, Infinity, "Should be Infinity!" + messages.indexOf(key)); + } + + if (key.isNegativeInfinity) { + is(event.data, -Infinity, "Should be -Infinity!" + messages.indexOf(key)); + } + + if (key.shouldCompare || key.shouldEqual) { + ok(event.data == key.compareValue, + "Values don't compare! " + messages.indexOf(key)); + } + + if (key.shouldEqual) { + ok(event.data === key.compareValue, + "Values don't equal! " + messages.indexOf(key)); + } + + if (key.jsonValue) { + is(JSON.stringify(event.data), key.jsonValue, + "Object stringification inconsistent!" + messages.indexOf(key)); + } + + if (event.data == "testFinished") { + is(index, messages.length, "Didn't see the right number of messages!"); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + worker.postMessage("start"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_loadEncoding.html b/dom/workers/test/test_loadEncoding.html new file mode 100644 index 0000000000..e7afb2fc82 --- /dev/null +++ b/dom/workers/test/test_loadEncoding.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 484305 - Load workers as UTF-8</title> + <meta http-equiv="content-type" content="text/html; charset=KOI8-R"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=484305">Bug 484305 - Load workers as UTF-8</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var canonical = String.fromCharCode(0x41F, 0x440, 0x438, 0x432, 0x435, 0x442); +ok(document.inputEncoding === "KOI8-R", "Document encoding is KOI8-R"); + +// Worker sends two strings, one with `canonical` encoded in KOI8-R and one as UTF-8. +// Since Worker scripts should always be decoded using UTF-8, even if the owning document's charset is different, the UTF-8 decode should match, while KOI8-R should fail. +var counter = 0; +var worker = new Worker("loadEncoding_worker.js"); +worker.onmessage = function(e) { + if (e.data.encoding === "KOI8-R") { + ok(e.data.text !== canonical, "KOI8-R decoded text should not match"); + } else if (e.data.encoding === "UTF-8") { + ok(e.data.text === canonical, "UTF-8 decoded text should match"); + } + counter++; + if (counter === 2) + SimpleTest.finish(); +} + +worker.onerror = function(e) { + ok(false, "Worker error"); + SimpleTest.finish(); +} +</script> + +</pre> +</body> +</html> diff --git a/dom/workers/test/test_loadError.html b/dom/workers/test/test_loadError.html new file mode 100644 index 0000000000..dec29d00c4 --- /dev/null +++ b/dom/workers/test/test_loadError.html @@ -0,0 +1,67 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +"use strict"; + +function nextTest() { + (function(){ + function workerfunc() { + var subworker = new Worker("about:blank"); + subworker.onerror = function(e) { + e.preventDefault(); + postMessage("ERROR"); + } + } + var b = new Blob([workerfunc+'workerfunc();']); + var u = URL.createObjectURL(b); + function callworker(i) { + var w = new Worker(u); + URL.revokeObjectURL(u); + w.onmessage = function(e) { + is(i, 0, 'Message received'); + is(e.data, "ERROR", + "Should catch the error when loading inner script"); + if (++i < 2) callworker(i); + else SimpleTest.finish(); + }; + w.onerror = function(e) { + is(i, 1, 'OnError received'); + SimpleTest.finish(); + } + } + callworker(0); + })(); +} + +try { + var worker = new Worker("about:blank"); + worker.onerror = function(e) { + e.preventDefault(); + nextTest(); + } + + worker.onmessage = function(event) { + ok(false, "Shouldn't get a message!"); + SimpleTest.finish(); + } +} catch (e) { + ok(false, "This should not happen."); +} + + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_location.html b/dom/workers/test/test_location.html new file mode 100644 index 0000000000..55c94ae1cf --- /dev/null +++ b/dom/workers/test/test_location.html @@ -0,0 +1,72 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Location +--> +<head> + <title>Test for DOM Worker Location</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var thisFilename = "test_location.html"; + var workerFilename = "location_worker.js"; + + var href = window.location.href + var queryPos = href.lastIndexOf(window.location.search); + var baseHref = href.substr(0, href.substr(0, queryPos).lastIndexOf("/") + 1); + + var path = window.location.pathname; + var basePath = path.substr(0, path.lastIndexOf("/") + 1); + + var strings = { + "toString": baseHref + workerFilename, + "href": baseHref + workerFilename, + "protocol": window.location.protocol, + "host": window.location.host, + "hostname": window.location.hostname, + "port": window.location.port, + "pathname": basePath + workerFilename, + "search": "", + "hash": "", + "origin": "http://mochi.test:8888" + }; + + var lastSlash = href.substr(0, queryPos).lastIndexOf("/") + 1; + is(thisFilename, + href.substr(lastSlash, queryPos - lastSlash), + "Correct filename "); + + var worker = new Worker(workerFilename); + + worker.onmessage = function(event) { + if (event.data.string == "testfinished") { + SimpleTest.finish(); + return; + } + ok(event.data.string in strings, event.data.string); + is(event.data.value, strings[event.data.string], event.data.string); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_longThread.html b/dom/workers/test/test_longThread.html new file mode 100644 index 0000000000..80602419ca --- /dev/null +++ b/dom/workers/test/test_longThread.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + const numThreads = 5; + var doneThreads = 0; + + function onmessage(event) { + switch (event.data) { + case "done": + if (++doneThreads == numThreads) { + ok(true, "All messages received from workers"); + SimpleTest.finish(); + } + break; + default: + ok(false, "Unexpected message"); + SimpleTest.finish(); + } + } + + function onerror(event) { + ok(false, "Worker had an error"); + SimpleTest.finish(); + } + + for (var i = 0; i < numThreads; i++) { + var worker = new Worker("longThread_worker.js"); + worker.onmessage = onmessage; + worker.onerror = onerror; + worker.postMessage("start"); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_multi_sharedWorker.html b/dom/workers/test/test_multi_sharedWorker.html new file mode 100644 index 0000000000..7bfbeaa9e9 --- /dev/null +++ b/dom/workers/test/test_multi_sharedWorker.html @@ -0,0 +1,241 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script class="testbody" type="text/javascript"> + "use strict"; + + const basePath = + location.pathname.substring(0, + location.pathname.lastIndexOf("/") + 1); + const baseURL = location.origin + basePath; + + const frameRelativeURL = "multi_sharedWorker_frame.html"; + const frameAbsoluteURL = baseURL + frameRelativeURL; + const workerAbsoluteURL = + baseURL + "multi_sharedWorker_sharedWorker.js"; + + const storedData = "0123456789abcdefghijklmnopqrstuvwxyz"; + const errorMessage = "Error: Expected"; + const errorLineno = 34; + + let testGenerator = (function*() { + SimpleTest.waitForExplicitFinish(); + + window.addEventListener("message", function(event) { + if (typeof(event.data) == "string") { + info(event.data); + } else { + sendToGenerator(event); + } + }); + + let frame1 = document.getElementById("frame1"); + frame1.src = frameRelativeURL; + frame1.onload = sendToGenerator; + + yield undefined; + + frame1 = frame1.contentWindow; + + let frame2 = document.getElementById("frame2"); + frame2.src = frameAbsoluteURL; + frame2.onload = sendToGenerator; + + yield undefined; + + frame2 = frame2.contentWindow; + + let data = { + command: "start" + }; + + frame1.postMessage(data, "*"); + frame2.postMessage(data, "*"); + + let event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "connect", "Got a connect message"); + + data = { + command: "retrieve" + }; + frame1.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored yet"); + + frame2.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame2, "Second window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored yet"); + + data = { + command: "store", + data: storedData + }; + frame2.postMessage(data, "*"); + + data = { + command: "retrieve" + }; + frame1.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, storedData, "Got stored data"); + + // This will generate two MessageEvents, one for each window. + let sawFrame1Error = false; + let sawFrame2Error = false; + + data = { + command: "error" + }; + frame1.postMessage(data, "*"); + + // First event. + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "worker-error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + // Second event + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "worker-error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + is(sawFrame1Error, true, "Saw error for frame1"); + is(sawFrame2Error, true, "Saw error for frame2"); + + // This will generate two MessageEvents, one for each window. + sawFrame1Error = false; + sawFrame2Error = false; + + data = { + command: "error" + }; + frame1.postMessage(data, "*"); + + // First event. + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + is(event.data.isErrorEvent, true, "Frame got an ErrorEvent"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + // Second event + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + is(event.data.isErrorEvent, true, "Frame got an ErrorEvent"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + is(sawFrame1Error, true, "Saw error for frame1"); + is(sawFrame2Error, true, "Saw error for frame2"); + + // Try a shared worker in a different origin. + frame1 = document.getElementById("frame1"); + frame1.src = "http://example.org" + basePath + frameRelativeURL; + frame1.onload = sendToGenerator; + yield undefined; + + frame1 = frame1.contentWindow; + + data = { + command: "retrieve" + }; + frame1.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored yet"); + + frame2.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame2, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, storedData, "Got stored data"); + + window.removeEventListener("message", sendToGenerator); + + SimpleTest.finish(); + })(); + + let sendToGenerator = testGenerator.next.bind(testGenerator); + + </script> + </head> + <body onload="testGenerator.next();"> + <iframe id="frame1"></iframe> + <iframe id="frame2"></iframe> + </body> +</html> diff --git a/dom/workers/test/test_multi_sharedWorker_lifetimes_bfcache.html b/dom/workers/test/test_multi_sharedWorker_lifetimes_bfcache.html new file mode 100644 index 0000000000..346950020f --- /dev/null +++ b/dom/workers/test/test_multi_sharedWorker_lifetimes_bfcache.html @@ -0,0 +1,151 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script class="testbody" type="text/javascript"> + "use strict"; + const windowRelativeURL = "multi_sharedWorker_frame_bfcache.html"; + // This suffix will be used as a search query parameter when we are + // navigating from navigate.html to the multi_sharedWorker_frame_bfcache + // page again. Since we are not using history.back() we are not loading + // that page from bfcache, but instead loading it anew, while the page + // we loaded initially stays in bfcache. In order to not kick out the + // page that is currently in the bfcache, we need to use a different + // BroadcastChannel. So we use search query param as part of + // BroadcastChannel's name. + const suffix = "?3"; + const storedData = "0123456789abcdefghijklmnopqrstuvwxyz"; + var bc, bc2, bc3; + SimpleTest.waitForExplicitFinish(); + + + function postToWorker(aBc, workerMessage) { + aBc.postMessage({command: "postToWorker", workerMessage}); + } + + let testGenerator = (function*() { + + bc = new BroadcastChannel("bugSharedWorkerLiftetime"); + bc3 = new BroadcastChannel("bugSharedWorkerLiftetime" + suffix); + bc.onmessage = (event) => { + var msg = event.data; + var command = msg.command; + if (command == "debug") { + info(msg.message); + } else if (command == "fromWorker" || command == "loaded") { + sendToGenerator(msg.workerMessage); + } + } + bc3.onmessage = (event) => { + var msg = event.data; + var command = msg.command; + if (command == "finished") { + bc.close(); + bc3.close(); + bc2.close(); + SimpleTest.finish(); + } else if (command == "debug") { + info(msg.message); + } else if (command == "fromWorker" || command == "loaded") { + sendToGenerator(msg.workerMessage); + } + } + bc2 = new BroadcastChannel("navigate"); + bc2.onmessage = (event) => { + if (event.data.command == "loaded") { + sendToGenerator(); + } + } + + // Open the window + window.open(windowRelativeURL, "testWin", "noopener"); + yield undefined; + + postToWorker(bc, { command: "retrieve" }); + + var msg = yield undefined; + is(msg.type, "result", "Got a result message"); + is(msg.data, undefined, "No data stored"); + + postToWorker(bc, { command: "store", data: storedData }); + postToWorker(bc, { command: "retrieve" }); + + msg = yield undefined; + is(msg.type, "result", "Got a result message"); + is(msg.data, storedData, "Got stored data"); + + + // Navigate when the bfcache is enabled. + info("Navigating to a different page"); + bc.postMessage({command: "navigate", location: "navigate.html"}); + yield undefined; + + for (let i = 0; i < 3; i++) { + info("Running GC"); + SpecialPowers.exactGC(sendToGenerator); + yield undefined; + + // It seems using SpecialPowers.executeSoon() would make the + // entryGlobal being the BrowserChildGlobal (and that would make the + // baseURI in the location assignment below being incorrect); + // setTimeout on the otherhand ensures the entryGlobal is this + // window. + info("Waiting the event queue to clear"); + setTimeout(sendToGenerator, 0); + yield undefined; + } + + info("Navigating to " + windowRelativeURL); + bc2.postMessage({command: "navigate", location: windowRelativeURL + suffix}); + yield undefined; + + postToWorker(bc3, { command: "retrieve" }); + + msg = yield undefined; + is(msg.type, "result", "Got a result message"); + is(msg.data, storedData, "Still have data stored"); + + bc3.postMessage({command: "finish"}); + })(); + + let sendToGenerator = testGenerator.next.bind(testGenerator); + + function runTest() { + if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.pushPermissions([{'type': 'storageAccessAPI', 'allow': 1, 'context': document}], () =>{ + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [ + ["fission.bfcacheInParent", true], + ["privacy.partition.always_partition_third_party_non_cookie_storage", false], + ]}, () => { + testGenerator.next(); + }); + }).then(() => { + SpecialPowers.removePermission("3rdPartyStorage^http://mochi.test:8888", "http://mochi.xorigin-test:8888"); + }); + }); + } else { + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + testGenerator.next(); + }); + } + } + </script> + </head> + <body onload="runTest()"> + </body> +</html> diff --git a/dom/workers/test/test_multi_sharedWorker_lifetimes_nobfcache.html b/dom/workers/test/test_multi_sharedWorker_lifetimes_nobfcache.html new file mode 100644 index 0000000000..5049ead1a9 --- /dev/null +++ b/dom/workers/test/test_multi_sharedWorker_lifetimes_nobfcache.html @@ -0,0 +1,126 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script class="testbody" type="text/javascript"> + "use strict"; + + function runTest() { + const windowRelativeURL = "multi_sharedWorker_frame_nobfcache.html"; + const storedData = "0123456789abcdefghijklmnopqrstuvwxyz"; + var bc, bc2; + bc = new BroadcastChannel("bugSharedWorkerLiftetime"); + bc.onmessage = (event) => { + var msg = event.data; + var command = msg.command; + if (command == "finished") { + bc.close(); + bc2.close(); + SimpleTest.finish(); + } else if (command == "debug") { + info(msg.message); + } else if (command == "fromWorker" || command == "loaded") { + sendToGenerator(msg.workerMessage); + } + } + bc2 = new BroadcastChannel("navigate"); + bc2.onmessage = (event) => { + if (event.data.command == "loaded") { + sendToGenerator(); + } + } + + function postToWorker(workerMessage) { + bc.postMessage({command: "postToWorker", workerMessage}); + } + + let testGenerator = (function*() { + SimpleTest.waitForExplicitFinish(); + + // Open the window + window.open(windowRelativeURL, "testWin", "noopener"); + yield undefined; + + // Retrieve data from worker + postToWorker({ command: "retrieve" }); + + let msg = yield undefined; + + // Verify there is no data stored + is(msg.type, "result", "Got a result message"); + is(msg.data, undefined, "No data stored yet"); + + // Store data, and retrieve it + postToWorker({ command: "store", data: storedData }); + postToWorker({ command: "retrieve" }); + + msg = yield undefined; + // Verify there is data stored + is(msg.type, "result", "Got a result message"); + is(msg.data, storedData, "Got stored data"); + + + info("Navigating to a different page"); + // Current subpage should not go into bfcache because of the Cache-Control + // headers we have set. + bc.postMessage({command: "navigate", location: "navigate.html"}); + yield undefined; + + info("Navigating to " + windowRelativeURL); + bc2.postMessage({command: "navigate", location: windowRelativeURL }); + yield undefined; + + postToWorker({ command: "retrieve" }); + + msg = yield undefined; + is(msg.type, "result", "Got a result message"); + is(msg.data, undefined, "No data stored"); + + postToWorker({ command: "store", data: storedData }); + postToWorker({ command: "retrieve" }); + + msg = yield undefined; + is(msg.type, "result", "Got a result message"); + is(msg.data, storedData, "Got stored data"); + + bc.postMessage({command: "finish"}); + })(); + + let sendToGenerator = testGenerator.next.bind(testGenerator); + testGenerator.next(); + } + + SimpleTest.waitForExplicitFinish(); + if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]], + }).then(() => { + SpecialPowers.pushPermissions([{'type': 'storageAccessAPI', 'allow': 1, 'context': document}], () =>{ + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + runTest(); + }).then(() => { + SpecialPowers.removePermission("3rdPartyStorage^http://mochi.test:8888", "http://mochi.xorigin-test:8888"); + }); + }); + }); + } else { + runTest(); + } + + </script> + </head> + <body> + </body> +</html> diff --git a/dom/workers/test/test_navigator.html b/dom/workers/test/test_navigator.html new file mode 100644 index 0000000000..a9ca9cad66 --- /dev/null +++ b/dom/workers/test/test_navigator.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Navigator +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script> + ok(!self.isSecureContext, "This test should not be running in a secure context"); +</script> +<script type="text/javascript" src="test_navigator.js"></script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_navigator.js b/dom/workers/test/test_navigator.js new file mode 100644 index 0000000000..c1eab3aa89 --- /dev/null +++ b/dom/workers/test/test_navigator.js @@ -0,0 +1,10 @@ +SimpleTest.waitForExplicitFinish(); + +// This test loads in an iframe, to ensure that the navigator instance is +// loaded with the correct value of the preference. +SpecialPowers.pushPrefEnv({ set: [["dom.netinfo.enabled", true]] }, () => { + let iframe = document.createElement("iframe"); + iframe.id = "f1"; + iframe.src = "test_navigator_iframe.html"; + document.body.appendChild(iframe); +}); diff --git a/dom/workers/test/test_navigator_iframe.html b/dom/workers/test/test_navigator_iframe.html new file mode 100644 index 0000000000..907739e90a --- /dev/null +++ b/dom/workers/test/test_navigator_iframe.html @@ -0,0 +1,24 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Sub-tests of DOM Worker Navigator tests. +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="text/javascript" src="test_navigator_iframe.js"></script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_navigator_iframe.js b/dom/workers/test/test_navigator_iframe.js new file mode 100644 index 0000000000..248d7ed2cd --- /dev/null +++ b/dom/workers/test/test_navigator_iframe.js @@ -0,0 +1,65 @@ +var worker = new Worker("navigator_worker.js"); + +var is = window.parent.is; +var ok = window.parent.ok; +var SimpleTest = window.parent.SimpleTest; + +worker.onmessage = function (event) { + var args = JSON.parse(event.data); + + if (args.name == "testFinished") { + SimpleTest.finish(); + return; + } + + if (typeof navigator[args.name] == "undefined") { + ok(false, "Navigator has no '" + args.name + "' property!"); + return; + } + + if (args.name === "languages") { + is( + navigator.languages.toString(), + args.value.toString(), + "languages matches" + ); + return; + } + + const objectProperties = [ + "connection", + "gpu", + "locks", + "mediaCapabilities", + "storage", + ]; + + if (objectProperties.includes(args.name)) { + is( + typeof navigator[args.name], + typeof args.value, + `${args.name} type matches` + ); + return; + } + + is( + navigator[args.name], + args.value, + "Mismatched navigator string for " + args.name + "!" + ); +}; + +worker.onerror = function (event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); +}; + +var { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var isNightly = AppConstants.NIGHTLY_BUILD; +var isRelease = AppConstants.RELEASE_OR_BETA; +var isAndroid = AppConstants.platform == "android"; + +worker.postMessage({ isNightly, isRelease, isAndroid }); diff --git a/dom/workers/test/test_navigator_languages.html b/dom/workers/test/test_navigator_languages.html new file mode 100644 index 0000000000..e4b6fec9a6 --- /dev/null +++ b/dom/workers/test/test_navigator_languages.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Navigator +--> +<head> + <title>Test for DOM Worker Navigator.languages</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var tests = [ + { expectedLanguages: 'en,it', inputLanguages: 'en,it' }, + { expectedLanguages: 'it,en,fr', inputLanguages: 'it,en,fr' }, + { expectedLanguages: SpecialPowers.Services.locale.webExposedLocales[0], inputLanguages: '' }, + { expectedLanguages: 'en,it', inputLanguages: 'en,it' }, + ]; + var test; + function runTests() { + if (!tests.length) { + worker.postMessage('finish'); + SimpleTest.finish(); + return; + } + + test = tests.shift(); + SpecialPowers.pushPrefEnv({"set": [["intl.accept_languages", test.inputLanguages]]}, function() { + worker.postMessage(true); + }); + } + + SimpleTest.waitForExplicitFinish(); + + var worker = new Worker("navigator_languages_worker.js"); + + worker.onmessage = function(event) { + is(event.data.toString(), navigator.languages.toString(), "The languages match."); + is(event.data.toString(), test.expectedLanguages, "This is the correct result."); + runTests(); + } + + runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_navigator_secureContext.html b/dom/workers/test/test_navigator_secureContext.html new file mode 100644 index 0000000000..ac5ceb6628 --- /dev/null +++ b/dom/workers/test/test_navigator_secureContext.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Navigator +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script> + ok(self.isSecureContext, "This test should be running in a secure context"); +</script> +<script type="text/javascript" src="test_navigator.js"></script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_navigator_workers_hardwareConcurrency.html b/dom/workers/test/test_navigator_workers_hardwareConcurrency.html new file mode 100644 index 0000000000..e80c211ce7 --- /dev/null +++ b/dom/workers/test/test_navigator_workers_hardwareConcurrency.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Navigator.hardwareConcurrency</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + "use strict"; + + SimpleTest.waitForExplicitFinish(); + + function getWorkerHardwareConcurrency(onmessage) { + var script = "postMessage(navigator.hardwareConcurrency)"; + var url = URL.createObjectURL(new Blob([script])); + var w = new Worker(url); + w.onmessage = onmessage; + } + + function resistFingerprinting(value) { + return SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting", value]]}); + } + + getWorkerHardwareConcurrency(e => { + var x = e.data; + is(typeof x, "number", "hardwareConcurrency should be a number."); + ok(x > 0, "hardwareConcurrency should be greater than 0."); + + resistFingerprinting(true).then(() => { + getWorkerHardwareConcurrency(msg => { + const y = msg.data; + ok(y === 2, "hardwareConcurrency should always be 2 when we're resisting fingerprinting."); + + resistFingerprinting(false).then(() => { + getWorkerHardwareConcurrency(msg1 => { + const z = msg1.data; + ok(z === x, "hardwareConcurrency should be the same as before we were resisting fingerprinting."); + + SimpleTest.finish(); + }); + }); + }); + }); + }); + + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_newError.html b/dom/workers/test/test_newError.html new file mode 100644 index 0000000000..9dd0889df8 --- /dev/null +++ b/dom/workers/test/test_newError.html @@ -0,0 +1,33 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("newError_worker.js"); + + worker.onmessage = function(event) { + ok(false, "Shouldn't get a message!"); + SimpleTest.finish(); + } + + worker.onerror = function(event) { + is(event.message, "Error: foo!", "Got wrong error message!"); + event.preventDefault(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_notification.html b/dom/workers/test/test_notification.html new file mode 100644 index 0000000000..0171dea0f2 --- /dev/null +++ b/dom/workers/test/test_notification.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 916893</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=916893">Bug 916893</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function runTest() { + MockServices.register(); + var w = new Worker("notification_worker.js"); + w.onmessage = function(e) { + if (e.data.type === 'finish') { + MockServices.unregister(); + SimpleTest.finish(); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'is') { + is(e.data.test1, e.data.test2, e.data.message); + } + } + + SimpleTest.waitForExplicitFinish(); + // turn on testing pref (used by notification.cpp, and mock the alerts + SpecialPowers.setBoolPref("notification.prompt.testing", true); + w.postMessage('start') + } + + SimpleTest.waitForExplicitFinish(); + runTest(); +</script> +</body> +</html> diff --git a/dom/workers/test/test_notification_child.html b/dom/workers/test/test_notification_child.html new file mode 100644 index 0000000000..4f9523a03f --- /dev/null +++ b/dom/workers/test/test_notification_child.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 916893 - Test Notifications in child workers.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=916893">Bug 916893</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show event."); + function runTest() { + MockServices.register(); + var w = new Worker("notification_worker_child-parent.js"); + w.onmessage = function(e) { + if (e.data.type === 'finish') { + MockServices.unregister(); + SimpleTest.finish(); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'is') { + is(e.data.test1, e.data.test2, e.data.message); + } + } + + SimpleTest.waitForExplicitFinish(); + // turn on testing pref (used by notification.cpp, and mock the alerts + SpecialPowers.setBoolPref("notification.prompt.testing", true); + w.postMessage('start') + } + + SimpleTest.waitForExplicitFinish(); + runTest(); +</script> +</body> +</html> diff --git a/dom/workers/test/test_notification_permission.html b/dom/workers/test/test_notification_permission.html new file mode 100644 index 0000000000..904cfcdef2 --- /dev/null +++ b/dom/workers/test/test_notification_permission.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 916893 - Make sure error is fired on Notification if permission is denied.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=916893">Bug 916893</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show event."); + function runTest() { + MockServices.register(); + var w = new Worker("notification_permission_worker.js"); + w.onmessage = function(e) { + if (e.data.type === 'finish') { + SpecialPowers.setBoolPref("notification.prompt.testing.allow", true); + MockServices.unregister(); + SimpleTest.finish(); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'is') { + is(e.data.test1, e.data.test2, e.data.message); + } + } + + SimpleTest.waitForExplicitFinish(); + // turn on testing pref (used by notification.cpp, and mock the alerts + SpecialPowers.setBoolPref("notification.prompt.testing", true); + SpecialPowers.setBoolPref("notification.prompt.testing.allow", false); + w.postMessage('start') + } + + SimpleTest.waitForExplicitFinish(); + runTest(); +</script> +</body> +</html> diff --git a/dom/workers/test/test_onLine.html b/dom/workers/test/test_onLine.html new file mode 100644 index 0000000000..b0429e7d96 --- /dev/null +++ b/dom/workers/test/test_onLine.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 925437: online/offline events tests. + +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/licenses/publicdomain/ +--> +<head> + <title>Test for Bug 925437 (worker online/offline events)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=925437">Mozilla Bug 925437</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="text/javascript"> + +addLoadEvent(function() { + var w = new Worker("onLine_worker.js"); + + w.onmessage = function(e) { + if (e.data.type === 'ready') { + // XXX Important trick here. + // + // Setting iosvc.offline would trigger a sync notifyObservers call, and if + // there exists a preloaded about:newtab (see tabbrowser._handleNewTab), + // that tab will be notified. + // + // This implies a sync call across different tabGroups, and will hit the + // assertion in SchedulerGroup::ValidateAccess(). So use executeSoon to + // re-dispatch an unlabeled runnable to the event queue. + SpecialPowers.executeSoon(doTest); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'finished') { + SimpleTest.finish(); + } + } + + function doTest() { + var iosvc = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + iosvc.manageOfflineStatus = false; + + info("setting iosvc.offline = true"); + iosvc.offline = true; + + info("setting iosvc.offline = false"); + iosvc.offline = false; + + info("setting iosvc.offline = true"); + iosvc.offline = true; + + for (var i = 0; i < 10; ++i) { + iosvc.offline = !iosvc.offline; + } + + info("setting iosvc.offline = false"); + w.postMessage('lastTest'); + iosvc.offline = false; + } +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</body> +</html> diff --git a/dom/workers/test/test_promise.html b/dom/workers/test/test_promise.html new file mode 100644 index 0000000000..7c3ef09a98 --- /dev/null +++ b/dom/workers/test/test_promise.html @@ -0,0 +1,43 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Promise object in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + var worker = new Worker("promise_worker.js"); + + worker.onmessage = function(event) { + + if (event.data.type == 'finish') { + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } + } + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + }; + + worker.postMessage(true); + } + + SimpleTest.waitForExplicitFinish(); + runTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_promise_resolved_with_string.html b/dom/workers/test/test_promise_resolved_with_string.html new file mode 100644 index 0000000000..8c0b0aca49 --- /dev/null +++ b/dom/workers/test/test_promise_resolved_with_string.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1027221 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1027221</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1027221 **/ + // Set up a permanent atom + SimpleTest.waitForExplicitFinish(); + var x = "x"; + // Trigger some incremental gc + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + + // Kick off a worker that uses this same atom + var w = new Worker("data:text/plain,Promise.resolve('x').then(function() { postMessage(1); });"); + // Maybe trigger some more incremental gc + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + + w.onmessage = function() { + ok(true, "Got here"); + SimpleTest.finish(); + }; + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1027221">Mozilla Bug 1027221</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_readableStream_when_closing.html b/dom/workers/test/test_readableStream_when_closing.html new file mode 100644 index 0000000000..24d5bf3821 --- /dev/null +++ b/dom/workers/test/test_readableStream_when_closing.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for ReadableStream+fetch when the worker is closing</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<body> + <script> + +function workerCode() { + onmessage = () => { + const BIG_BUFFER_SIZE = 1000000; + const fibStream = new ReadableStream({ + start(controller) {}, + + pull(controller) { + const buffer = new Uint8Array(BIG_BUFFER_SIZE); + buffer.fill(42); + controller.enqueue(buffer); + } + }); + + const r = new Response(fibStream); + + const p = r.blob(); + self.postMessage("reading"); + + p.then(() => { + // really? + }); + } +} + +SimpleTest.waitForExplicitFinish(); + +const b = new Blob([workerCode+'workerCode();']); +const url = URL.createObjectURL(b); +const w = new Worker(url); +w.onmessage = function(e) { + ok(true, 'Worker is reading'); + + const wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"]. + getService(Ci.nsIWorkerDebuggerManager); + wdm.addListener({ + onUnregister (dbg) { + if (dbg.url == url) { + ok(true, "Debugger with url " + url + " should be unregistered."); + wdm.removeListener(this); + SimpleTest.finish(); + } + } + }); + + w.terminate(); +} +w.postMessage("start"); + </script> +</body> +</html> diff --git a/dom/workers/test/test_recursion.html b/dom/workers/test/test_recursion.html new file mode 100644 index 0000000000..8e38a80351 --- /dev/null +++ b/dom/workers/test/test_recursion.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads Recursion</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + // Intermittently triggers one assertion on Mac (bug 848098). + if (navigator.platform.indexOf("Mac") == 0) { + SimpleTest.expectAssertions(0, 1); + } + + const testCount = 2; + var errorCount = 0; + + var worker = new Worker("recursion_worker.js"); + + function done() { + worker.terminate(); + SimpleTest.finish(); + } + + worker.onmessage = function(event) { + if (event.data == "Done") { + ok(true, "correct message"); + } + else { + ok(false, "Bad message: " + event.data); + } + done(); + } + + worker.onerror = function(event) { + event.preventDefault(); + if (event.message == "too much recursion") { + ok(true, "got correct error message"); + ++errorCount; + } + else { + ok(false, "got bad error message: " + event.message); + done(); + } + } + + for (var i = 0; i < testCount; i++) { + worker.postMessage(""); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_recursiveOnerror.html b/dom/workers/test/test_recursiveOnerror.html new file mode 100644 index 0000000000..e6c040439d --- /dev/null +++ b/dom/workers/test/test_recursiveOnerror.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const filename = "http://mochi.test:8888/tests/dom/workers/test/" + + "recursiveOnerror_worker.js"; + const errors = [ + { message: "Error: 2", lineno: 6 }, + { message: "Error: 1", lineno: 10 } + ] + + var errorCount = 0; + + var worker = new Worker("recursiveOnerror_worker.js"); + worker.postMessage("go"); + + worker.onerror = function(event) { + event.preventDefault(); + + ok(errorCount < errors.length, "Correct number of error events"); + const error = errors[errorCount++]; + + is(event.message, error.message, "Correct message"); + is(event.filename, filename, "Correct filename"); + is(event.lineno, error.lineno, "Correct lineno"); + + if (errorCount == errors.length) { + SimpleTest.finish(); + } + } + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_referrer.html b/dom/workers/test/test_referrer.html new file mode 100644 index 0000000000..a95fe91809 --- /dev/null +++ b/dom/workers/test/test_referrer.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the referrer of workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function test_mainScript() { + var worker = new Worker("referrer.sjs?worker"); + worker.onmessage = function() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'referrer.sjs?result', true); + xhr.onload = function() { + is(xhr.responseText, location.href, "The referrer has been sent."); + next(); + } + xhr.send(); + } + worker.postMessage(42); + } + + function test_importScript() { + var worker = new Worker("worker_referrer.js"); + worker.onmessage = function(e) { + is(e.data, location.href.replace("test_referrer.html", "worker_referrer.js").split("?")[0], "The referrer has been sent."); + next(); + } + worker.postMessage(42); + } + + var tests = [ test_mainScript, test_importScript ]; + function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + next(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_referrer_header_worker.html b/dom/workers/test/test_referrer_header_worker.html new file mode 100644 index 0000000000..6d350801cb --- /dev/null +++ b/dom/workers/test/test_referrer_header_worker.html @@ -0,0 +1,39 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test the referrer of workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv( + {"set": [ + ['security.mixed_content.block_display_content', false], + ['security.mixed_content.block_active_content', false] + ]}, + function() { + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], test); + }); + + function test() { + function messageListener(event) { + // eslint-disable-next-line no-eval + eval(event.data); + } + window.addEventListener("message", messageListener); + + var ifr = document.createElement('iframe'); + ifr.setAttribute('src', 'https://example.com/tests/dom/workers/test/referrer_worker.html'); + document.body.appendChild(ifr); + } + </script> +</body> +</html> diff --git a/dom/workers/test/test_resolveWorker-assignment.html b/dom/workers/test/test_resolveWorker-assignment.html new file mode 100644 index 0000000000..2f3be19668 --- /dev/null +++ b/dom/workers/test/test_resolveWorker-assignment.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="application/javascript"> + window.Worker = 17; // resolve through assignment + + var desc = Object.getOwnPropertyDescriptor(window, "Worker"); + ok(typeof desc === "object" && desc !== null, "Worker property must exist"); + + is(desc.value, 17, "Overwrite didn't work correctly"); + is(desc.enumerable, false, + "Initial descriptor was non-enumerable, and [[Put]] changes the " + + "property value but not its enumerability"); + is(desc.configurable, true, + "Initial descriptor was configurable, and [[Put]] changes the " + + "property value but not its configurability"); + is(desc.writable, true, + "Initial descriptor was writable, and [[Put]] changes the " + + "property value but not its writability"); + </script> + </body> +</html> diff --git a/dom/workers/test/test_resolveWorker.html b/dom/workers/test/test_resolveWorker.html new file mode 100644 index 0000000000..0d91a9f90a --- /dev/null +++ b/dom/workers/test/test_resolveWorker.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="application/javascript"> + window.Worker; // resolve not through assignment + Worker = 17; + + var desc = Object.getOwnPropertyDescriptor(window, "Worker"); + ok(typeof desc === "object" && desc !== null, "Worker property must exist"); + + is(desc.value, 17, "Overwrite didn't work correctly"); + is(desc.enumerable, false, + "Initial descriptor was non-enumerable, and [[Put]] changes the " + + "property value but not its enumerability"); + is(desc.configurable, true, + "Initial descriptor was configurable, and [[Put]] changes the " + + "property value but not its configurability"); + is(desc.writable, true, + "Initial descriptor was writable, and [[Put]] changes the " + + "property value but not its writability"); + </script> + </body> +</html> diff --git a/dom/workers/test/test_rvals.html b/dom/workers/test/test_rvals.html new file mode 100644 index 0000000000..799c42779d --- /dev/null +++ b/dom/workers/test/test_rvals.html @@ -0,0 +1,35 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 911085</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("rvals_worker.js"); + + worker.onmessage = function(event) { + if (event.data == 'ignore') return; + + if (event.data == 'finished') { + is(worker.terminate(), undefined, "Terminate() returns 'undefined'"); + SimpleTest.finish(); + return; + } + + ok(event.data, "something good returns 'undefined' in workers"); + }; + + is(worker.postMessage(42), undefined, "PostMessage() returns 'undefined' on main thread"); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_setTimeoutWith0.html b/dom/workers/test/test_setTimeoutWith0.html new file mode 100644 index 0000000000..8685eacfe5 --- /dev/null +++ b/dom/workers/test/test_setTimeoutWith0.html @@ -0,0 +1,19 @@ +<html> +<head> + <title>Test for DOM Worker setTimeout and strings containing 0</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> + +var a = new Worker('worker_setTimeoutWith0.js'); +a.onmessage = function(e) { + is(e.data, 2, "We want to see 2 here"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +</script> +</body> +</html> diff --git a/dom/workers/test/test_sharedWorker.html b/dom/workers/test/test_sharedWorker.html new file mode 100644 index 0000000000..0a3cde2d32 --- /dev/null +++ b/dom/workers/test/test_sharedWorker.html @@ -0,0 +1,71 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + </head> + <body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + <script class="testbody"> + "use strict"; + + const href = window.location.href; + const filename = "sharedWorker_sharedWorker.js"; + const sentMessage = "ping"; + const errorFilename = href.substring(0, href.lastIndexOf("/") + 1) + + filename; + const errorLine = 98; + const errorColumn = 11; + + var worker = new SharedWorker(filename); + + ok(worker instanceof SharedWorker, "Got SharedWorker instance"); + ok(!("postMessage" in worker), "SharedWorker has no 'postMessage'"); + ok(worker.port instanceof MessagePort, + "Shared worker has MessagePort"); + + var receivedMessage; + var receivedError; + + worker.port.onmessage = function(event) { + ok(event instanceof MessageEvent, "Got a MessageEvent"); + ok(event.target === worker.port, + "MessageEvent has correct 'target' property"); + is(event.data, sentMessage, "Got correct message"); + ok(receivedMessage === undefined, "Haven't gotten message yet"); + receivedMessage = event.data; + if (receivedError) { + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(event instanceof ErrorEvent, "Got an ErrorEvent"); + is(event.message, "Error: " + sentMessage, "Got correct error"); + is(event.filename, errorFilename, "Got correct filename"); + is(event.lineno, errorLine, "Got correct lineno"); + is(event.colno, errorColumn, "Got correct column"); + ok(receivedError === undefined, "Haven't gotten error yet"); + receivedError = event.message; + event.preventDefault(); + if (receivedMessage) { + SimpleTest.finish(); + } + }; + + worker.port.postMessage(sentMessage); + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> + </body> +</html> diff --git a/dom/workers/test/test_sharedWorker_lifetime.html b/dom/workers/test/test_sharedWorker_lifetime.html new file mode 100644 index 0000000000..b987f0d7cf --- /dev/null +++ b/dom/workers/test/test_sharedWorker_lifetime.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for MessagePort and SharedWorkers</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + </head> + <body> + <script class="testbody" type="text/javascript"> + +var gced = false; + +var sw = new SharedWorker('sharedWorker_lifetime.js'); +sw.port.onmessage = function(event) { + ok(gced, "The SW is still alive also after GC"); + SimpleTest.finish(); +} + +sw = null; +SpecialPowers.forceGC(); +gced = true; + +SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_sharedWorker_ports.html b/dom/workers/test/test_sharedWorker_ports.html new file mode 100644 index 0000000000..6befd4920c --- /dev/null +++ b/dom/workers/test/test_sharedWorker_ports.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for MessagePort and SharedWorkers</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + </head> + <body> + <script class="testbody" type="text/javascript"> + +var sw1 = new SharedWorker('sharedWorker_ports.js'); +sw1.port.onmessage = function(event) { + if (event.data.type == "connected") { + ok(true, "The SharedWorker is alive."); + + var sw2 = new SharedWorker('sharedWorker_ports.js'); + sw1.port.postMessage("Port from the main-thread!", [sw2.port]); + return; + } + + if (event.data.type == "status") { + ok(event.data.test, event.data.msg); + return; + } + + if (event.data.type == "finish") { + info("Finished!"); + ok(sw1.port, "The port still exists"); + sw1.port.foo = sw1; // Just a test to see if we leak. + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_sharedWorker_privateBrowsing.html b/dom/workers/test/test_sharedWorker_privateBrowsing.html new file mode 100644 index 0000000000..e93d65b3b8 --- /dev/null +++ b/dom/workers/test/test_sharedWorker_privateBrowsing.html @@ -0,0 +1,104 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SharedWorker - Private Browsing</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<body> + +<script type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); +var mainWindow; + +var contentPage = "http://mochi.test:8888/chrome/dom/workers/test/empty.html"; + +function testOnWindow(aIsPrivate, aCallback) { + var win = mainWindow.OpenBrowserWindow({private: aIsPrivate}); + win.addEventListener("load", function() { + win.addEventListener("DOMContentLoaded", function onInnerLoad() { + if (win.content.location.href != contentPage) { + BrowserTestUtils.startLoadingURIString(win.gBrowser, contentPage); + return; + } + + win.removeEventListener("DOMContentLoaded", onInnerLoad, true); + SimpleTest.executeSoon(function() { aCallback(win); }); + }, true); + }, {capture: true, once: true}); +} + +function setupWindow() { + mainWindow = window.browsingContext.topChromeWindow; + runTest(); +} + +var wN; +var wP; + +function doTests() { + testOnWindow(false, function(aWin) { + wN = aWin; + + testOnWindow(true, function(win) { + wP = win; + + var sharedWorker1 = new wP.content.SharedWorker('sharedWorker_privateBrowsing.js'); + sharedWorker1.port.onmessage = function(event) { + is(event.data, 1, "Only 1 sharedworker expected in the private window"); + + var sharedWorker2 = new wN.content.SharedWorker('sharedWorker_privateBrowsing.js'); + sharedWorker2.port.onmessage = function(event1) { + is(event1.data, 1, "Only 1 sharedworker expected in the normal window"); + + var sharedWorker3 = new wP.content.SharedWorker('sharedWorker_privateBrowsing.js'); + sharedWorker3.port.onmessage = function(event2) { + is(event2.data, 2, "Only 2 sharedworker expected in the private window"); + runTest(); + } + } + } + }); + }); +} + +function doSystemSharedWorkerTest() { + try { + let chromeShared = + new wP.SharedWorker("chrome://mochitests/content/dom/workers/test/sharedWorker_privateBrowsing.js"); + ok(true, "system SharedWorker created without throwing or crashing!"); + } catch (_ex) { + ok(false, "system SharedWorker should not throw or crash"); + } + runTest(); +} + +var steps = [ + setupWindow, + doTests, + doSystemSharedWorkerTest, +]; + +function runTest() { + if (!steps.length) { + wN.close(); + wP.close(); + + SimpleTest.finish(); + return; + } + + var step = steps.shift(); + step(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["browser.startup.page", 0], + ["browser.startup.homepage_override.mstone", "ignore"], +]}, runTest); + +</script> +</body> +</html> diff --git a/dom/workers/test/test_sharedWorker_thirdparty.html b/dom/workers/test/test_sharedWorker_thirdparty.html new file mode 100644 index 0000000000..caeb122bba --- /dev/null +++ b/dom/workers/test/test_sharedWorker_thirdparty.html @@ -0,0 +1,54 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for SharedWorker in 3rd Party Iframes</title> + <script src="/tests/SimpleTest/SimpleTest.js"> </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + <script class="testbody"> + + function testThirdPartyFrame(name) { + return new Promise(resolve => { + // Let's use a window, loading the same origin, in order to have the new + // cookie-policy applied. + let w = window.open("sharedWorker_thirdparty_window.html?name=" + name); + window.addEventListener('message', function messageListener(evt) { + if (evt.data.name !== name) { + return; + } + w.close(); + window.removeEventListener('message', messageListener); + resolve(evt.data.result); + }); + }); + } + + const COOKIE_BEHAVIOR_ACCEPT = 0; + const COOKIE_BEHAVIOR_REJECTFOREIGN = 1; + + add_task(async function allowed() { + await SpecialPowers.pushPrefEnv({ set: [ + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_ACCEPT] + ]}); + let result = await testThirdPartyFrame('allowed'); + ok(result === 'allowed', + 'SharedWorker should be allowed when 3rd party iframes can access storage'); + }); + + add_task(async function blocked() { + await SpecialPowers.pushPrefEnv({ set: [ + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_REJECTFOREIGN] + ]}); + let result = await testThirdPartyFrame('blocked'); + ok(result === 'blocked', + 'SharedWorker should not be allowed when 3rd party iframes are denied storage'); + }); + + </script> +</body> +</html> diff --git a/dom/workers/test/test_sharedworker_event_listener_leaks.html b/dom/workers/test/test_sharedworker_event_listener_leaks.html new file mode 100644 index 0000000000..4016cdda48 --- /dev/null +++ b/dom/workers/test/test_sharedworker_event_listener_leaks.html @@ -0,0 +1,51 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450358 - Test SharedWorker event listener leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Manipulate SharedWorker objects in the frame's context. +// Its important here that we create a listener callback from +// the DOM objects back to the frame's global in order to +// exercise the leak condition. +async function useSharedWorker(contentWindow) { + contentWindow.messageCount = 0; + + let sw = new contentWindow.SharedWorker("data:application/javascript,self.onconnect = e => e.ports[0].postMessage({})"); + sw.onerror = _ => { + contentWindow.errorCount += 1; + } + await new Promise(resolve => { + sw.port.onmessage = e => { + contentWindow.messageCount += 1; + resolve(); + }; + }); + + is(contentWindow.messageCount, 1, "message should be received"); +} + +async function runTest() { + try { + await checkForEventListenerLeaks("SharedWorker", useSharedWorker); + } catch (e) { + ok(false, e); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_shutdownCheck.xhtml b/dom/workers/test/test_shutdownCheck.xhtml new file mode 100644 index 0000000000..257b37fe32 --- /dev/null +++ b/dom/workers/test/test_shutdownCheck.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window title="Worker shutdown check" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + +SimpleTest.waitForExplicitFinish() + +const URL = "worker_shutdownCheck.js"; + +function checkWorker() { + const wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"]. + getService(Ci.nsIWorkerDebuggerManager); + + let e = wdm.getWorkerDebuggerEnumerator(); + while (e.hasMoreElements()) { + let dbg = e.getNext().QueryInterface(Ci.nsIWorkerDebugger); + if (dbg.url == URL) { + return true; + } + } + + return false; +} + +new Promise(resolve => { + var w = new Worker(URL); + ok(checkWorker(), "We have the worker"); + w.onmessage = () => { resolve(); } +}).then(() => { + info("Waiting..."); + + // We don't know if the worker thread is able to shutdown when calling + // CC/GC. Better to check again in case. + function checkGC() { + Cu.forceCC(); + Cu.forceGC(); + if (!checkWorker()) { + ok(true, "We don't have the worker"); + SimpleTest.finish(); + return; + } + setTimeout(checkGC, 200); + } + + checkGC(); +}); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_simpleThread.html b/dom/workers/test/test_simpleThread.html new file mode 100644 index 0000000000..aee5765170 --- /dev/null +++ b/dom/workers/test/test_simpleThread.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("simpleThread_worker.js"); + + worker.addEventListener("message",function(event) { + is(event.target, worker); + switch (event.data) { + case "no-op": + break; + case "started": + is(gotErrors, true); + worker.postMessage("no-op"); + worker.postMessage("stop"); + break; + case "stopped": + worker.postMessage("no-op"); + SimpleTest.finish(); + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }); + + var gotErrors = false; + worker.onerror = function(event) { + event.preventDefault(); + is(event.target, worker); + is(event.message, "uncaught exception: Bad message: asdf"); + + worker.onerror = function(otherEvent) { + otherEvent.preventDefault(); + is(otherEvent.target, worker); + is(otherEvent.message, "ReferenceError: Components is not defined"); + gotErrors = true; + + worker.onerror = function(oneMoreEvent) { + ok(false, "Worker had an error:" + oneMoreEvent.message); + SimpleTest.finish(); + }; + }; + }; + + worker.postMessage("asdf"); + worker.postMessage("components"); + worker.postMessage("start"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_sourcemap_header.html b/dom/workers/test/test_sourcemap_header.html new file mode 100644 index 0000000000..250b30f079 --- /dev/null +++ b/dom/workers/test/test_sourcemap_header.html @@ -0,0 +1,22 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker + SourceMap header</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script type="application/javascript" src="dom_worker_helper.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> + +<iframe id="worker-frame"></iframe> +</body> +<script type="text/javascript" src="sourcemap_header.js"></script> +</html> diff --git a/dom/workers/test/test_subworkers_suspended.html b/dom/workers/test/test_subworkers_suspended.html new file mode 100644 index 0000000000..01157671d7 --- /dev/null +++ b/dom/workers/test/test_subworkers_suspended.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for sub workers+bfcache behavior</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + + + + /** + * - main page opens testUrl1 + * - testUrl1 ---"onpageshow"---> to main page + * - main page ---"startWorker"---> testUrl1 + * - testUrl1 starts workers, also ---"verifyCacheData"---> main page + * - main page ---"changeLocation"---> testUrl1 + * - testUrl1 navigated to testUrl2 + * - testUrl2 ---"onpageshow"---> to main page + * - main page ---"startWorker"---> testUrl2 + * - testUrl2 starts workers, also ---"verifyCacheData"---> main page + * - main page ---"goBack"---> testUrl2 + * - testUrl2 navigates back to testUrl1 + * - testUrl1 ---"onpageshow"---> to main page + * - main page checks cache data and ---"finish"---> testUrl2 + * - testUrl1 ---"finished"---> to main page + */ + var testUrl1 = "window_suspended.html?page1Shown"; + var counter = 0; + const SUB_WORKERS = 3; + + function cacheData() { + return caches.open("test") + .then(function(cache) { + return cache.match("http://mochi.test:888/foo"); + }) + .then(function(response) { + return response.text(); + }); + } + + function runTest() { + var bc1 = new BroadcastChannel("page1Shown"); + bc1.onmessage = async (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + info(`Main page, received command=${command}`); + if (command == "onpageshow") { + info("Page1Shown: " + msg.location); + // First time this page is shown. + if (counter == 0) { + ok(!msg.persisted, "test page should have been persisted initially"); + var workerMessage = { type: "page1", count: SUB_WORKERS }; + bc1.postMessage({command: "startWorker", workerMessage}); + } else { + is(msg.persisted, true, "test page should have been persisted in pageshow"); + var promise = new Promise((resolve, reject) => { + info("Waiting a few seconds..."); + setTimeout(resolve, 10000); + }); + + promise.then(function() { + info("Retrieving data from cache..."); + return cacheData(); + }) + + .then(function(content) { + is(content.indexOf("page1-"), 0, "We have data from the worker"); + }) + .then(function() { + bc1.postMessage({command: "finish"}); + }); + } + counter++; + } else if (command == "workerMessage") { + is(msg.workerMessage, "ready", "We want to receive: -ready-"); + } else if (command == "verifyCacheData") { + var content = await cacheData(); + is(content.indexOf("page1-"), 0, "We have data from the worker"); + bc1.postMessage({command: "changeLocation"}); + } else if (command == "finished") { + bc1.close(); + bc2.close(); + SimpleTest.finish(); + } + } + var bc2 = new BroadcastChannel("page2Shown"); + bc2.onmessage = async (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "onpageshow") { + info("Page1Shown: " + msg.location); + var workerMessage = { type: "page2" }; + bc2.postMessage({command: "startWorker", workerMessage}); + } else if (command == "workerMessage") { + is(msg.workerMessage, "ready", "We want to receive: -ready-"); + } else if (command == "verifyCacheData") { + var content = await cacheData(); + is(content, "page2-0", "We have data from the second worker"); + bc2.postMessage({command: "goBack"}); + } + } + + SpecialPowers.pushPrefEnv({ set: [ + ["dom.caches.testing.enabled", true], + // If Fission is disabled, the pref is no-op. + ["fission.bfcacheInParent", true], + ] }, + function() { + window.open(testUrl1, "", "noopener"); + }); + + } + + if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]], + }).then(() => { + SpecialPowers.pushPermissions([{'type': 'storageAccessAPI', 'allow': 1, 'context': document}], () =>{ + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + runTest(); + }).then(() => { + SpecialPowers.removePermission("3rdPartyStorage^http://mochi.test:8888", "http://mochi.xorigin-test:8888"); + }); + }); + }); + } else { + runTest(); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + + </script> +</body> +</html> diff --git a/dom/workers/test/test_suspend.html b/dom/workers/test/test_suspend.html new file mode 100644 index 0000000000..9ab1a6a7ec --- /dev/null +++ b/dom/workers/test/test_suspend.html @@ -0,0 +1,188 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + SimpleTest.waitForExplicitFinish(); + + /** + * - main page tells subpage to call startWorker() + * - subpage starts worker + * - worker calls setInterval() and keeps calling postMessage() + * - onmessage(), as setup by the subpage, calls messageCallback + * - when messageCallback gets called more than 25 times + * - subpage gets navigated to blank.html + * - blank page posts message to main page, and main page calls suspendCallback() + * - suspendCallback() schedules waitInterval() to be fired off every second + * - after 5 times, it clears the interval and navigates subpage back + * - suspend_window subpage starts receiving messages again and + * does a final call to messageCallback() + * - finishTest() is called + */ + + var lastCount; + + var suspended = false; + var resumed = false; + var finished = false; + var suspendBlankPageCurrentlyShowing = false; + + var interval; + var oldMessageCount; + var waitCount = 0; + + var bcSuspendWindow, bcSuspendBlank; + + function runTest() { + bcSuspendWindow = new BroadcastChannel("suspendWindow"); + bcSuspendWindow.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + var data = msg.data; + if (command == "loaded") { + if (finished) { + return; + } + bcSuspendWindow.postMessage({command: "startWorker"}); + } else if (command == "messageCallback") { + messageCallback(data); + } else if (command == "errorCallback") { + errorCallback(data); + } else if (command == "finished") { + SimpleTest.finish(); + } + } + + bcSuspendBlank = new BroadcastChannel("suspendBlank"); + bcSuspendBlank.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "loaded") { + suspendBlankPageCurrentlyShowing = true; + if (suspended) { + badOnloadCallback(); + } else { + suspendCallback(); + } + } else if (command == "pagehide") { + suspendBlankPageCurrentlyShowing = false; + } + } + + // If Fission is disabled, the pref is no-op. + SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}, () => { + window.open("suspend_window.html", "testWin", "noopener"); + }); + } + + function finishTest() { + if (finished) { + return; + } + finished = true; + bcSuspendWindow.postMessage({command: "finish"}); + } + + function waitInterval() { + if (finished) { + return; + } + ok(suspendBlankPageCurrentlyShowing, "correct page is showing"); + is(suspended, true, "Not suspended?"); + is(resumed, false, "Already resumed?!"); + is(lastCount, oldMessageCount, "Received a message while suspended!"); + if (++waitCount == 5) { + clearInterval(interval); + resumed = true; + bcSuspendBlank.postMessage({command: "navigateBack"}); + } + } + + function badOnloadCallback() { + if (finished) { + return; + } + ok(false, "We don't want suspend_window.html to fire a new load event, we want it to come out of the bfcache!"); + finishTest(); + } + + function suspendCallback() { + if (finished) { + return; + } + ok(suspendBlankPageCurrentlyShowing, "correct page is showing"); + is(suspended, false, "Already suspended?"); + is(resumed, false, "Already resumed?"); + suspended = true; + oldMessageCount = lastCount; + interval = setInterval(waitInterval, 1000); + } + + function messageCallback(data) { + if (finished) { + return; + } + + if (!suspended) { + ok(lastCount === undefined || lastCount == data - 1, + "Got good data, lastCount = " + lastCount + ", data = " + data); + lastCount = data; + if (lastCount == 25) { + bcSuspendWindow.postMessage({command: "navigate"}); + } + return; + } + + ok(!suspendBlankPageCurrentlyShowing, "correct page is showing"); + is(resumed, true, "Got message before resumed!"); + is(lastCount, data - 1, "Missed a message, suspend failed!"); + finishTest(); + } + + function errorCallback(data) { + if (finished) { + return; + } + ok(false, "testWin had an error: '" + data + "'"); + finishTest(); + } + + if (isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the BroadcastChannel used to + // communicate with the opened windows works in xorigin tests. Otherwise, + // the iframe containing this page is isolated from first-party storage access, + // which isolates BroadcastChannel communication. + SpecialPowers.wrap(document).notifyUserGestureActivation(); + SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.always_partition_third_party_non_cookie_storage", false]], + }).then(() => { + SpecialPowers.pushPermissions([{'type': 'storageAccessAPI', 'allow': 1, 'context': document}], () =>{ + SpecialPowers.wrap(document).requestStorageAccess().then(() => { + runTest(); + }).then(() => { + SpecialPowers.removePermission("3rdPartyStorage^http://mochi.test:8888", "http://mochi.xorigin-test:8888"); + }); + }); + }); + } else { + runTest(); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_terminate.html b/dom/workers/test/test_terminate.html new file mode 100644 index 0000000000..c19d65770b --- /dev/null +++ b/dom/workers/test/test_terminate.html @@ -0,0 +1,100 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker terminate feature +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + + var messageCount = 0; + var intervalCount = 0; + + var interval; + + var worker; + + function messageListener(event) { + is(event.data, "Still alive!", "Correct message!"); + if (++messageCount == 20) { + ok(worker.onmessage === messageListener, + "Correct listener before terminate"); + + worker.terminate(); + + var exception = false; + try { + worker.addEventListener("message", messageListener); + } + catch (e) { + exception = true; + } + is(exception, false, "addEventListener didn't throw after terminate"); + + exception = false; + try { + worker.removeEventListener("message", messageListener); + } + catch (e) { + exception = true; + } + is(exception, false, "removeEventListener didn't throw after terminate"); + + exception = false; + try { + worker.postMessage("foo"); + } + catch (e) { + exception = true; + } + is(exception, false, "postMessage didn't throw after terminate"); + + exception = false; + try { + worker.terminate(); + } + catch (e) { + exception = true; + } + is(exception, false, "terminate didn't throw after terminate"); + + ok(worker.onmessage === messageListener, + "Correct listener after terminate"); + + worker.onmessage = function(msg) { } + + interval = setInterval(testCount, 1000); + } + } + + function testCount() { + is(messageCount, 20, "Received another message after terminated!"); + if (intervalCount++ == 5) { + clearInterval(interval); + SimpleTest.finish(); + } + } + + worker = new Worker("terminate_worker.js"); + worker.onmessage = messageListener; + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_threadErrors.html b/dom/workers/test/test_threadErrors.html new file mode 100644 index 0000000000..1eb33244ba --- /dev/null +++ b/dom/workers/test/test_threadErrors.html @@ -0,0 +1,64 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + const expectedErrorCount = 4; + + function messageListener(event) { + ok(false, "Unexpected message: " + event.data); + SimpleTest.finish(); + }; + + var actualErrorCount = 0; + var failedWorkers = []; + + function errorListener(event) { + event.preventDefault(); + + if (failedWorkers.includes(event.target)) { + ok(false, "Seen an extra error from this worker"); + SimpleTest.finish(); + return; + } + + failedWorkers.push(event.target); + actualErrorCount++; + + if (actualErrorCount == expectedErrorCount) { + ok(true, "all errors correctly detected"); + SimpleTest.finish(); + } + }; + + for (var i = 1; i <= expectedErrorCount; i++) { + var worker = new Worker("threadErrors_worker" + i + ".js"); + worker.onmessage = messageListener; + worker.onerror = errorListener; + worker.postMessage("Hi"); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_threadTimeouts.html b/dom/workers/test/test_threadTimeouts.html new file mode 100644 index 0000000000..93a8d9243c --- /dev/null +++ b/dom/workers/test/test_threadTimeouts.html @@ -0,0 +1,60 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("threadTimeouts_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker); + switch (event.data) { + case "timeoutFinished": + event.target.postMessage("startInterval"); + break; + case "intervalFinished": + event.target.postMessage("cancelInterval"); + break; + case "intervalCanceled": + worker.postMessage("startExpression"); + break; + case "expressionFinished": + SimpleTest.finish(); + break; + default: + ok(false, "Unexpected message"); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + }; + + worker.postMessage("startTimeout"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_throwingOnerror.html b/dom/workers/test/test_throwingOnerror.html new file mode 100644 index 0000000000..0ed1f74247 --- /dev/null +++ b/dom/workers/test/test_throwingOnerror.html @@ -0,0 +1,54 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads Recursion</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("throwingOnerror_worker.js"); + + var errors = ["foo", "bar"]; + + worker.onerror = function(event) { + event.preventDefault(); + var found = false; + for (var index in errors) { + if (event.message == "uncaught exception: " + errors[index]) { + errors.splice(index, 1); + found = true; + break; + } + } + is(found, true, "Unexpected error!"); + }; + + worker.onmessage = function(event) { + is(errors.length, 0, "Didn't see expected errors!"); + SimpleTest.finish(); + }; + + for (var i = 0; i < 2; i++) { + worker.postMessage(""); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_timeoutTracing.html b/dom/workers/test/test_timeoutTracing.html new file mode 100644 index 0000000000..8e64564b72 --- /dev/null +++ b/dom/workers/test/test_timeoutTracing.html @@ -0,0 +1,47 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("timeoutTracing_worker.js"); + + worker.onmessage = function(event) { + // begin + worker.onmessage = null; + + // 1 second should be enough to crash. + window.setTimeout(function() { + ok(true, "Didn't crash!"); + SimpleTest.finish(); + }, 1000); + + var os = SpecialPowers.Cc["@mozilla.org/observer-service;1"] + .getService(SpecialPowers.Ci.nsIObserverService); + os.notifyObservers(null, "memory-pressure", "heap-minimize"); + } + + worker.onerror = function(event) { + ok(false, "I was expecting a crash, not an error"); + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_transferable.html b/dom/workers/test/test_transferable.html new file mode 100644 index 0000000000..ac490f369a --- /dev/null +++ b/dom/workers/test/test_transferable.html @@ -0,0 +1,123 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker transferable objects +--> +<head> + <title>Test for DOM Worker transferable objects</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + function test1(sizes) { + if (!sizes.length) { + runTests(); + return; + } + + var size = sizes.pop(); + + var worker = new Worker("transferable_worker.js"); + worker.onmessage = function(event) { + ok(event.data.status, event.data.event); + if (!event.data.status) { + runTests(); + return; + } + + if ("notEmpty" in event.data && "byteLength" in event.data.notEmpty) { + ok(event.data.notEmpty.byteLength != 0, + "P: NotEmpty object received: " + event.data.notEmpty.byteLength); + } + + if (!event.data.last) + return; + + test1(sizes); + } + worker.onerror = function(event) { + ok(false, "No errors!"); + } + + try { + worker.postMessage(42, true); + ok(false, "P: PostMessage - Exception for wrong type"); + } catch(e) { + ok(true, "P: PostMessage - Exception for wrong type"); + } + + try { + ab = new ArrayBuffer(size); + worker.postMessage(42,[ab, ab]); + ok(false, "P: PostMessage - Exception for duplicate"); + } catch(e) { + ok(true, "P: PostMessage - Exception for duplicate"); + } + + var ab = new ArrayBuffer(size); + ok(ab.byteLength == size, "P: The size is: " + size + " == " + ab.byteLength); + worker.postMessage({ data: 0, timeout: 0, ab, cb: ab, size }, [ab]); + ok(ab.byteLength == 0, "P: PostMessage - The size is: 0 == " + ab.byteLength) + } + + function test2() { + var worker = new Worker("transferable_worker.js"); + worker.onmessage = function(event) { + ok(event.data.status, event.data.event); + if (!event.data.status) { + runTests(); + return; + } + + if ("notEmpty" in event.data && "byteLength" in event.data.notEmpty) { + ok(event.data.notEmpty.byteLength != 0, + "P: NotEmpty object received: " + event.data.notEmpty.byteLength); + } + + if (event.data.last) { + runTests(); + } + } + worker.onerror = function(event) { + ok(false, "No errors!"); + } + + var f = new Float32Array([0,1,2,3]); + ok(f.byteLength != 0, "P: The size is: " + f.byteLength + " is not 0"); + worker.postMessage({ event: "P: postMessage with Float32Array", status: true, + size: 4, notEmpty: f, bc: [ f, f, { dd: f } ] }, [f.buffer]); + ok(f.byteLength == 0, "P: The size is: " + f.byteLength + " is 0"); + } + + var tests = [ + function() { test1([1024 * 1024 * 32, 128, 4]); }, + test2 + ]; + function runTests() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_worker_interfaces.html b/dom/workers/test/test_worker_interfaces.html new file mode 100644 index 0000000000..b051e01242 --- /dev/null +++ b/dom/workers/test/test_worker_interfaces.html @@ -0,0 +1,19 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Validate Interfaces Exposed to Workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="worker_driver.js"></script> +</head> +<body> +<script> + ok(!self.isSecureContext, "This test should not be running in a secure context"); +</script> +<script class="testbody" type="text/javascript"> +workerTestExec("test_worker_interfaces.js"); +</script> +</body> +</html> diff --git a/dom/workers/test/test_worker_interfaces.js b/dom/workers/test/test_worker_interfaces.js new file mode 100644 index 0000000000..3ea89ad6b5 --- /dev/null +++ b/dom/workers/test/test_worker_interfaces.js @@ -0,0 +1,563 @@ +// This is a list of all interfaces that are exposed to workers. +// Please only add things to this list with great care and proper review +// from the associated module peers. + +// This file lists global interfaces we want exposed and verifies they +// are what we intend. Each entry in the arrays below can either be a +// simple string with the interface name, or an object with a 'name' +// property giving the interface name as a string, and additional +// properties which qualify the exposure of that interface. For example: +// +// [ +// "AGlobalInterface", // secure context only +// { name: "ExperimentalThing", release: false }, +// { name: "ReallyExperimentalThing", nightly: true }, +// { name: "DesktopOnlyThing", desktop: true }, +// { name: "FancyControl", xbl: true }, +// { name: "DisabledEverywhere", disabled: true }, +// ]; +// +// See createInterfaceMap() below for a complete list of properties. +// +// The values of the properties need to be literal true/false +// (e.g. indicating whether something is enabled on a particular +// channel/OS). If we ever end up in a situation where a propert +// value needs to depend on channel or OS, we will need to make sure +// we have that information before setting up the property lists. + +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +let wasmGlobalEntry = { + name: "WebAssembly", + insecureContext: true, + disabled: !getJSTestingFunctions().wasmIsSupportedByHardware(), +}; +let wasmGlobalInterfaces = [ + { name: "Module", insecureContext: true }, + { name: "Instance", insecureContext: true }, + { name: "Memory", insecureContext: true }, + { name: "Table", insecureContext: true }, + { name: "Global", insecureContext: true }, + { name: "CompileError", insecureContext: true }, + { name: "LinkError", insecureContext: true }, + { name: "RuntimeError", insecureContext: true }, + { name: "Function", insecureContext: true, nightly: true }, + { name: "Exception", insecureContext: true }, + { name: "Tag", insecureContext: true }, + { name: "compile", insecureContext: true }, + { name: "compileStreaming", insecureContext: true }, + { name: "instantiate", insecureContext: true }, + { name: "instantiateStreaming", insecureContext: true }, + { name: "validate", insecureContext: true }, +]; +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +let ecmaGlobals = [ + { name: "AggregateError", insecureContext: true }, + { name: "Array", insecureContext: true }, + { name: "ArrayBuffer", insecureContext: true }, + { name: "Atomics", insecureContext: true }, + { name: "BigInt", insecureContext: true }, + { name: "BigInt64Array", insecureContext: true }, + { name: "BigUint64Array", insecureContext: true }, + { name: "Boolean", insecureContext: true }, + { name: "DataView", insecureContext: true }, + { name: "Date", insecureContext: true }, + { name: "Error", insecureContext: true }, + { name: "EvalError", insecureContext: true }, + { name: "FinalizationRegistry", insecureContext: true }, + { name: "Float32Array", insecureContext: true }, + { name: "Float64Array", insecureContext: true }, + { name: "Function", insecureContext: true }, + { name: "Infinity", insecureContext: true }, + { name: "Int16Array", insecureContext: true }, + { name: "Int32Array", insecureContext: true }, + { name: "Int8Array", insecureContext: true }, + { name: "InternalError", insecureContext: true }, + { name: "Intl", insecureContext: true }, + { name: "JSON", insecureContext: true }, + { name: "Map", insecureContext: true }, + { name: "MediaCapabilities", insecureContext: true }, + { name: "MediaCapabilitiesInfo", insecureContext: true }, + { name: "Math", insecureContext: true }, + { name: "NaN", insecureContext: true }, + { name: "Number", insecureContext: true }, + { name: "Object", insecureContext: true }, + { name: "Promise", insecureContext: true }, + { name: "Proxy", insecureContext: true }, + { name: "RangeError", insecureContext: true }, + { name: "ReferenceError", insecureContext: true }, + { name: "Reflect", insecureContext: true }, + { name: "RegExp", insecureContext: true }, + { name: "Set", insecureContext: true }, + { + name: "SharedArrayBuffer", + insecureContext: true, + crossOringinIsolated: true, + }, + { name: "String", insecureContext: true }, + { name: "Symbol", insecureContext: true }, + { name: "SyntaxError", insecureContext: true }, + { name: "TypeError", insecureContext: true }, + { name: "Uint16Array", insecureContext: true }, + { name: "Uint32Array", insecureContext: true }, + { name: "Uint8Array", insecureContext: true }, + { name: "Uint8ClampedArray", insecureContext: true }, + { name: "URIError", insecureContext: true }, + { name: "WeakMap", insecureContext: true }, + { name: "WeakRef", insecureContext: true }, + { name: "WeakSet", insecureContext: true }, + wasmGlobalEntry, + { name: "decodeURI", insecureContext: true }, + { name: "decodeURIComponent", insecureContext: true }, + { name: "encodeURI", insecureContext: true }, + { name: "encodeURIComponent", insecureContext: true }, + { name: "escape", insecureContext: true }, + { name: "eval", insecureContext: true }, + { name: "globalThis", insecureContext: true }, + { name: "isFinite", insecureContext: true }, + { name: "isNaN", insecureContext: true }, + { name: "parseFloat", insecureContext: true }, + { name: "parseInt", insecureContext: true }, + { name: "undefined", insecureContext: true }, + { name: "unescape", insecureContext: true }, +]; +// IMPORTANT: Do not change the list above without review from +// a JavaScript Engine peer! + +// IMPORTANT: Do not change the list below without review from a DOM peer! +let interfaceNamesInGlobalScope = [ + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "AbortController", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "AbortSignal", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Blob", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "BroadcastChannel", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ByteLengthQueuingStrategy", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "Cache", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CacheStorage", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CanvasGradient", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CanvasPattern", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CloseEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CompressionStream", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CountQueuingStrategy", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Crypto", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CryptoKey" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CustomEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DecompressionStream", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DedicatedWorkerGlobalScope", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Directory", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMException", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMMatrix", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMMatrixReadOnly", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMPoint", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMPointReadOnly", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMQuad", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMRect", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMRectReadOnly", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMRequest", insecureContext: true, disabled: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "DOMStringList", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "EncodedVideoChunk", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ErrorEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Event", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "EventSource", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "EventTarget", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "File", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileList", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileReader", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileReaderSync", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemDirectoryHandle" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemFileHandle" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemHandle" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemSyncAccessHandle" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemWritableFileStream" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FontFace", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FontFaceSet", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FontFaceSetLoadEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FormData", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Headers", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBCursor", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBCursorWithValue", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBDatabase", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBFactory", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBIndex", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBKeyRange", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBObjectStore", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBOpenDBRequest", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBRequest", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBTransaction", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "IDBVersionChangeEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ImageBitmap", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ImageBitmapRenderingContext", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ImageData", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "Lock", + // IMPORTANT: Do not change this list without review from a DOM peer! + "LockManager", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "MessageChannel", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "MessageEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "MessagePort", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "NetworkInformation", insecureContext: true, disabled: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Notification", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "OffscreenCanvas", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "OffscreenCanvasRenderingContext2D", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Path2D", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Performance", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceEntry", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceMark", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceMeasure", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceObserver", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceObserverEntryList", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceResourceTiming", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceServerTiming", insecureContext: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ProgressEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PromiseRejectionEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ReadableByteStreamController", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ReadableStream", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ReadableStreamBYOBReader", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ReadableStreamBYOBRequest", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ReadableStreamDefaultController", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "ReadableStreamDefaultReader", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Request", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Response", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "RTCEncodedAudioFrame", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "RTCEncodedVideoFrame", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "RTCRtpScriptTransformer", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "RTCTransformEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Scheduler", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "StorageManager", fennec: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "SubtleCrypto" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskController", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskPriorityChangeEvent", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskSignal", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TextDecoder", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TextDecoderStream", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TextEncoder", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TextEncoderStream", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TextMetrics", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TransformStream", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { + name: "TransformStreamDefaultController", + insecureContext: true, + }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "XMLHttpRequest", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "XMLHttpRequestEventTarget", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "XMLHttpRequestUpload", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "URL", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "URLSearchParams", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "VideoColorSpace", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "VideoDecoder", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "VideoEncoder", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "VideoFrame", insecureContext: true, nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGL2RenderingContext", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLActiveInfo", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLBuffer", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLContextEvent", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLFramebuffer", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLProgram", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLQuery", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLRenderbuffer", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLRenderingContext", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLSampler", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLShader", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLShaderPrecisionFormat", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLSync", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLTexture", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLTransformFeedback", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLUniformLocation", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLVertexArrayObject", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebSocket", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebTransport", insecureContext: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebTransportBidirectionalStream", insecureContext: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebTransportDatagramDuplexStream", insecureContext: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebTransportError", insecureContext: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebTransportReceiveStream", insecureContext: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebTransportSendStream", insecureContext: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Worker", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WorkerGlobalScope", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WorkerLocation", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WorkerNavigator", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WritableStream", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WritableStreamDefaultController", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WritableStreamDefaultWriter", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "cancelAnimationFrame", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "close", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "console", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "name", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "onmessage", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "onmessageerror", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "onrtctransform", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "postMessage", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "requestAnimationFrame", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! +]; +// IMPORTANT: Do not change the list above without review from a DOM peer! + +// List of functions defined on the global by the test harness or this test +// file. +let testFunctions = [ + "ok", + "is", + "workerTestArrayEquals", + "workerTestDone", + "workerTestGetPermissions", + "workerTestGetHelperData", + "entryDisabled", + "createInterfaceMap", + "runTest", +]; + +function entryDisabled( + entry, + { + isNightly, + isEarlyBetaOrEarlier, + isRelease, + isDesktop, + isAndroid, + isInsecureContext, + isFennec, + isCrossOringinIsolated, + } +) { + return ( + entry.nightly === !isNightly || + (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) || + entry.desktop === !isDesktop || + (entry.android === !isAndroid && !entry.nightlyAndroid) || + entry.fennecOrDesktop === (isAndroid && !isFennec) || + entry.fennec === !isFennec || + entry.release === !isRelease || + // The insecureContext test is very purposefully converting + // entry.insecureContext to boolean, so undefined will convert to + // false. That way entries without an insecureContext annotation + // will get treated as "insecureContext: false", which means exposed + // only in secure contexts. + (isInsecureContext && !entry.insecureContext) || + entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier || + entry.crossOringinIsolated === !isCrossOringinIsolated || + entry.disabled + ); +} + +function createInterfaceMap(data, ...interfaceGroups) { + var interfaceMap = {}; + + function addInterfaces(interfaces) { + for (var entry of interfaces) { + if (typeof entry === "string") { + ok(!(entry in interfaceMap), "duplicate entry for " + entry); + interfaceMap[entry] = !data.isInsecureContext; + } else { + ok(!(entry.name in interfaceMap), "duplicate entry for " + entry.name); + ok(!("pref" in entry), "Bogus pref annotation for " + entry.name); + interfaceMap[entry.name] = !entryDisabled(entry, data); + } + } + } + + for (let interfaceGroup of interfaceGroups) { + addInterfaces(interfaceGroup); + } + + return interfaceMap; +} + +function runTest(parentName, parent, data, ...interfaceGroups) { + var interfaceMap = createInterfaceMap(data, ...interfaceGroups); + for (var name of Object.getOwnPropertyNames(parent)) { + // Ignore functions on the global that are part of the test (harness). + if (parent === self && testFunctions.includes(name)) { + continue; + } + ok( + interfaceMap[name], + "If this is failing: DANGER, are you sure you want to expose the new interface " + + name + + " to all webpages as a property of " + + parentName + + "? Do not make a change to this file without a " + + " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)" + ); + delete interfaceMap[name]; + } + for (var name of Object.keys(interfaceMap)) { + ok( + name in parent === interfaceMap[name], + name + + " should " + + (interfaceMap[name] ? "" : " NOT") + + " be defined on " + + parentName + ); + if (!interfaceMap[name]) { + delete interfaceMap[name]; + } + } + is( + Object.keys(interfaceMap).length, + 0, + "The following interface(s) are not enumerated: " + + Object.keys(interfaceMap).join(", ") + ); +} + +workerTestGetHelperData(function (data) { + runTest("self", self, data, ecmaGlobals, interfaceNamesInGlobalScope); + if (WebAssembly && !entryDisabled(wasmGlobalEntry, data)) { + runTest("WebAssembly", WebAssembly, data, wasmGlobalInterfaces); + } + workerTestDone(); +}); diff --git a/dom/workers/test/test_worker_interfaces_secureContext.html b/dom/workers/test/test_worker_interfaces_secureContext.html new file mode 100644 index 0000000000..7d26cd2131 --- /dev/null +++ b/dom/workers/test/test_worker_interfaces_secureContext.html @@ -0,0 +1,19 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Validate Interfaces Exposed to Workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="worker_driver.js"></script> +</head> +<body> +<script> + ok(self.isSecureContext, "This test should be running in a secure context"); +</script> +<script class="testbody" type="text/javascript"> +workerTestExec("test_worker_interfaces.js"); +</script> +</body> +</html> diff --git a/dom/workers/test/threadErrors_worker1.js b/dom/workers/test/threadErrors_worker1.js new file mode 100644 index 0000000000..c0ddade82c --- /dev/null +++ b/dom/workers/test/threadErrors_worker1.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Syntax error +onmessage = function(event) { + for (var i = 0; i < 10) { } +} diff --git a/dom/workers/test/threadErrors_worker2.js b/dom/workers/test/threadErrors_worker2.js new file mode 100644 index 0000000000..da79569def --- /dev/null +++ b/dom/workers/test/threadErrors_worker2.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Bad function error +onmessage = function (event) { + foopy(); +}; diff --git a/dom/workers/test/threadErrors_worker3.js b/dom/workers/test/threadErrors_worker3.js new file mode 100644 index 0000000000..e470680981 --- /dev/null +++ b/dom/workers/test/threadErrors_worker3.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Unhandled exception in body +onmessage = function (event) {}; + +throw new Error("Bah!"); diff --git a/dom/workers/test/threadErrors_worker4.js b/dom/workers/test/threadErrors_worker4.js new file mode 100644 index 0000000000..88b089aa3b --- /dev/null +++ b/dom/workers/test/threadErrors_worker4.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Throwing message listener +onmessage = function (event) { + throw new Error("Bah!"); +}; diff --git a/dom/workers/test/threadTimeouts_worker.js b/dom/workers/test/threadTimeouts_worker.js new file mode 100644 index 0000000000..27e514b391 --- /dev/null +++ b/dom/workers/test/threadTimeouts_worker.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var gTimeoutId; +var gTimeoutCount = 0; +var gIntervalCount = 0; + +function timeoutFunc() { + if (++gTimeoutCount > 1) { + throw new Error("Timeout called more than once!"); + } + postMessage("timeoutFinished"); +} + +function intervalFunc() { + if (++gIntervalCount == 2) { + postMessage("intervalFinished"); + } +} + +function messageListener(event) { + switch (event.data) { + case "startTimeout": + gTimeoutId = setTimeout(timeoutFunc, 2000); + clearTimeout(gTimeoutId); + gTimeoutId = setTimeout(timeoutFunc, 2000); + break; + case "startInterval": + gTimeoutId = setInterval(intervalFunc, 2000); + break; + case "cancelInterval": + clearInterval(gTimeoutId); + postMessage("intervalCanceled"); + break; + case "startExpression": + // eslint-disable-next-line no-implied-eval + setTimeout("this.postMessage('expressionFinished');", 2000); + break; + default: + throw "Bad message: " + event.data; + } +} + +addEventListener("message", messageListener, false); diff --git a/dom/workers/test/throwingOnerror_worker.js b/dom/workers/test/throwingOnerror_worker.js new file mode 100644 index 0000000000..47b727f56a --- /dev/null +++ b/dom/workers/test/throwingOnerror_worker.js @@ -0,0 +1,15 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onerror = function (event) { + throw "bar"; +}; + +var count = 0; +onmessage = function (event) { + if (!count++) { + throw "foo"; + } + postMessage(""); +}; diff --git a/dom/workers/test/timeoutTracing_worker.js b/dom/workers/test/timeoutTracing_worker.js new file mode 100644 index 0000000000..2eac367535 --- /dev/null +++ b/dom/workers/test/timeoutTracing_worker.js @@ -0,0 +1,16 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function (event) { + throw "No messages should reach me!"; +}; + +setInterval(function () { + postMessage("Still alive!"); +}, 20); +// eslint-disable-next-line no-implied-eval +setInterval(";", 20); + +postMessage("Begin!"); diff --git a/dom/workers/test/transferable_worker.js b/dom/workers/test/transferable_worker.js new file mode 100644 index 0000000000..d0fa41cad1 --- /dev/null +++ b/dom/workers/test/transferable_worker.js @@ -0,0 +1,40 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function (event) { + if ("notEmpty" in event.data && "byteLength" in event.data.notEmpty) { + postMessage({ + event: "W: NotEmpty object received: " + event.data.notEmpty.byteLength, + status: event.data.notEmpty.byteLength != 0, + last: false, + }); + } + + var ab = new ArrayBuffer(event.data.size); + postMessage({ + event: "W: The size is: " + event.data.size + " == " + ab.byteLength, + status: ab.byteLength == event.data.size, + last: false, + }); + + postMessage( + { + event: "W: postMessage with arrayBuffer", + status: true, + notEmpty: ab, + ab, + bc: [ab, ab, { dd: ab }], + }, + [ab] + ); + + postMessage({ + event: "W: The size is: 0 == " + ab.byteLength, + status: ab.byteLength == 0, + last: false, + }); + + postMessage({ event: "W: last one!", status: true, last: true }); +}; diff --git a/dom/workers/test/window_suspended.html b/dom/workers/test/window_suspended.html new file mode 100644 index 0000000000..ae5d25df58 --- /dev/null +++ b/dom/workers/test/window_suspended.html @@ -0,0 +1,71 @@ +<script> +const WORKER_URL = "worker_suspended.js"; +var testUrl2 = "window_suspended.html?page2Shown"; + +let cacheDataPromise = {}; +cacheDataPromise.promise = new Promise(resolve => { + cacheDataPromise.resolve = resolve; +}); +var bcName = location.search.split('?')[1]; +var bc = new BroadcastChannel(bcName); +if (bcName == "page1Shown") { + bc.onmessage = async (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "startWorker") { + // Create a worker and subworkers + let { worker, promise } = postMessageWorker(msg.workerMessage); + promise.then(function() { + bc.postMessage({command: "verifyCacheData"}); + return cacheDataPromise.promise; + }) + .then(function() { + location.href = testUrl2; + }); + } else if (command == "changeLocation") { + cacheDataPromise.resolve(); + } else if (command == "finish") { + bc.postMessage({command: "finished"}); + bc.close(); + window.close(); + } + } +} else if (bcName == "page2Shown") { + bc.onmessage = (msgEvent) => { + var msg = msgEvent.data; + var command = msg.command; + if (command == "startWorker") { + let { worker, promise } = postMessageWorker(msg.workerMessage); + promise.then(function() { + bc.postMessage({command: "verifyCacheData"}); + return cacheDataPromise.promise; + }) + .then(function() { + bc.close(); + history.back(); + }); + } else if (command == "goBack") { + cacheDataPromise.resolve(); + } + } +} + +function postMessageWorker(message) { + let worker = new window.Worker(WORKER_URL); + + var promise = new Promise((resolve, reject) => { + // Waiting until workers are ready + worker.addEventListener("message", function onmessage(msg) { + bc.postMessage({command: "workerMessage", workerMessage: msg.data}); + worker.removeEventListener("message", onmessage); + resolve(); + }); + worker.postMessage(message); + }); + return { worker, promise }; +} + +onpageshow = function(e) { + bc.postMessage({command: "onpageshow", persisted: e.persisted, location: location.href}); +} +</script> diff --git a/dom/workers/test/worker_bug1278777.js b/dom/workers/test/worker_bug1278777.js new file mode 100644 index 0000000000..f596ee978b --- /dev/null +++ b/dom/workers/test/worker_bug1278777.js @@ -0,0 +1,9 @@ +var xhr = new XMLHttpRequest(); +xhr.responseType = "blob"; +xhr.open("GET", "worker_bug1278777.js"); + +xhr.onload = function () { + postMessage(xhr.response instanceof Blob); +}; + +xhr.send(); diff --git a/dom/workers/test/worker_bug1301094.js b/dom/workers/test/worker_bug1301094.js new file mode 100644 index 0000000000..90fbe178b5 --- /dev/null +++ b/dom/workers/test/worker_bug1301094.js @@ -0,0 +1,11 @@ +onmessage = function (e) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", "worker_bug1301094.js", false); + xhr.onload = function () { + self.postMessage("OK"); + }; + + var fd = new FormData(); + fd.append("file", e.data); + xhr.send(fd); +}; diff --git a/dom/workers/test/worker_bug1824498.mjs b/dom/workers/test/worker_bug1824498.mjs new file mode 100644 index 0000000000..932bb530ac --- /dev/null +++ b/dom/workers/test/worker_bug1824498.mjs @@ -0,0 +1,4 @@ +/* eslint-disable import/no-unassigned-import */ +/* eslint-disable import/no-unresolved */ +import {} from "./foo"; +import {} from "./bar"; diff --git a/dom/workers/test/worker_consoleAndBlobs.js b/dom/workers/test/worker_consoleAndBlobs.js new file mode 100644 index 0000000000..e95a87fb83 --- /dev/null +++ b/dom/workers/test/worker_consoleAndBlobs.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var b = new Blob(["123"], { type: "foo/bar" }); +console.log({ msg: "consoleAndBlobs", blob: b }); diff --git a/dom/workers/test/worker_driver.js b/dom/workers/test/worker_driver.js new file mode 100644 index 0000000000..29a0d50025 --- /dev/null +++ b/dom/workers/test/worker_driver.js @@ -0,0 +1,84 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +// Utility script for writing worker tests. In your main document do: +// +// <script type="text/javascript" src="worker_driver.js"></script> +// <script type="text/javascript"> +// workerTestExec('myWorkerTestCase.js') +// </script> +// +// This will then spawn a worker, define some utility functions, and then +// execute the code in myWorkerTestCase.js. You can then use these +// functions in your worker-side test: +// +// ok() - like the SimpleTest assert +// is() - like the SimpleTest assert +// workerTestDone() - like SimpleTest.finish() indicating the test is complete +// +// There are also some functions for requesting information that requires +// SpecialPowers or other main-thread-only resources: +// +// workerTestGetVersion() - request the current version string from the MT +// workerTestGetUserAgent() - request the user agent string from the MT +// workerTestGetOSCPU() - request the navigator.oscpu string from the MT +// +// For an example see test_worker_interfaces.html and test_worker_interfaces.js. + +function workerTestExec(script) { + SimpleTest.waitForExplicitFinish(); + var worker = new Worker("worker_wrapper.js"); + worker.onmessage = function (event) { + if (event.data.type == "finish") { + SimpleTest.finish(); + } else if (event.data.type == "status") { + ok(event.data.status, event.data.msg); + } else if (event.data.type == "getHelperData") { + const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + const isNightly = AppConstants.NIGHTLY_BUILD; + const isEarlyBetaOrEarlier = AppConstants.EARLY_BETA_OR_EARLIER; + const isRelease = AppConstants.RELEASE_OR_BETA; + const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); + const isMac = AppConstants.platform == "macosx"; + const isWindows = AppConstants.platform == "win"; + const isAndroid = AppConstants.platform == "android"; + const isLinux = AppConstants.platform == "linux"; + const isInsecureContext = !window.isSecureContext; + // Currently, MOZ_APP_NAME is always "fennec" for all mobile builds, so we can't use AppConstants for this + const isFennec = + isAndroid && + SpecialPowers.Cc["@mozilla.org/android/bridge;1"].getService( + SpecialPowers.Ci.nsIAndroidBridge + ).isFennec; + const isCrossOriginIsolated = window.crossOriginIsolated; + + const result = { + isNightly, + isEarlyBetaOrEarlier, + isRelease, + isDesktop, + isMac, + isWindows, + isAndroid, + isLinux, + isInsecureContext, + isFennec, + isCrossOriginIsolated, + }; + + worker.postMessage({ + type: "returnHelperData", + result, + }); + } + }; + + worker.onerror = function (event) { + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage({ script }); +} diff --git a/dom/workers/test/worker_dynamicImport.mjs b/dom/workers/test/worker_dynamicImport.mjs new file mode 100644 index 0000000000..b5c50d30f7 --- /dev/null +++ b/dom/workers/test/worker_dynamicImport.mjs @@ -0,0 +1,2 @@ +/* eslint-disable import/no-unresolved */ +const { o } = await import("./404.js"); diff --git a/dom/workers/test/worker_referrer.js b/dom/workers/test/worker_referrer.js new file mode 100644 index 0000000000..ec9fb1f8a0 --- /dev/null +++ b/dom/workers/test/worker_referrer.js @@ -0,0 +1,9 @@ +onmessage = function () { + importScripts(["referrer.sjs?import"]); + var xhr = new XMLHttpRequest(); + xhr.open("GET", "referrer.sjs?result", true); + xhr.onload = function () { + postMessage(xhr.responseText); + }; + xhr.send(); +}; diff --git a/dom/workers/test/worker_setTimeoutWith0.js b/dom/workers/test/worker_setTimeoutWith0.js new file mode 100644 index 0000000000..91de9c5a73 --- /dev/null +++ b/dom/workers/test/worker_setTimeoutWith0.js @@ -0,0 +1,4 @@ +/* eslint-disable no-implied-eval */ +var x = 0; +setTimeout("x++; '\x00'; x++;"); +setTimeout("postMessage(x);"); diff --git a/dom/workers/test/worker_shutdownCheck.js b/dom/workers/test/worker_shutdownCheck.js new file mode 100644 index 0000000000..f51279daf6 --- /dev/null +++ b/dom/workers/test/worker_shutdownCheck.js @@ -0,0 +1 @@ +postMessage("Ok!"); diff --git a/dom/workers/test/worker_suspended.js b/dom/workers/test/worker_suspended.js new file mode 100644 index 0000000000..f2b4146cba --- /dev/null +++ b/dom/workers/test/worker_suspended.js @@ -0,0 +1,39 @@ +var count = 0; + +function do_magic(data) { + caches + .open("test") + .then(function (cache) { + return cache.put( + "http://mochi.test:888/foo", + new Response(data.type + "-" + count++) + ); + }) + .then(function () { + if (count == 1) { + postMessage("ready"); + } + + if (data.loop) { + setTimeout(function () { + do_magic(data); + }, 500); + } + }); +} + +onmessage = function (e) { + if (e.data.type == "page1") { + if (e.data.count > 0) { + var a = new Worker("worker_suspended.js"); + a.postMessage({ type: "page1", count: e.data - 1 }); + a.onmessage = function () { + postMessage("ready"); + }; + } else { + do_magic({ type: e.data.type, loop: true }); + } + } else if (e.data.type == "page2") { + do_magic({ type: e.data.type, loop: false }); + } +}; diff --git a/dom/workers/test/worker_wrapper.js b/dom/workers/test/worker_wrapper.js new file mode 100644 index 0000000000..6a630d92d0 --- /dev/null +++ b/dom/workers/test/worker_wrapper.js @@ -0,0 +1,79 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +// Worker-side wrapper script for the worker_driver.js helper code. See +// the comments at the top of worker_driver.js for more information. + +function ok(a, msg) { + dump("OK: " + !!a + " => " + a + ": " + msg + "\n"); + postMessage({ type: "status", status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + dump("IS: " + (a === b) + " => " + a + " | " + b + ": " + msg + "\n"); + postMessage({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + }); +} + +function workerTestArrayEquals(a, b) { + if (!Array.isArray(a) || !Array.isArray(b) || a.length != b.length) { + return false; + } + for (var i = 0, n = a.length; i < n; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +} + +function workerTestDone() { + postMessage({ type: "finish" }); +} + +function workerTestGetPermissions(permissions, cb) { + addEventListener("message", function workerTestGetPermissionsCB(e) { + if ( + e.data.type != "returnPermissions" || + !workerTestArrayEquals(permissions, e.data.permissions) + ) { + return; + } + removeEventListener("message", workerTestGetPermissionsCB); + cb(e.data.result); + }); + postMessage({ + type: "getPermissions", + permissions, + }); +} + +function workerTestGetHelperData(cb) { + addEventListener("message", function workerTestGetHelperDataCB(e) { + if (e.data.type !== "returnHelperData") { + return; + } + removeEventListener("message", workerTestGetHelperDataCB); + cb(e.data.result); + }); + postMessage({ + type: "getHelperData", + }); +} + +addEventListener("message", function workerWrapperOnMessage(e) { + removeEventListener("message", workerWrapperOnMessage); + var data = e.data; + try { + importScripts(data.script); + } catch (ex) { + postMessage({ + type: "status", + status: false, + msg: "worker failed to import " + data.script + "; error: " + ex.message, + }); + } +}); diff --git a/dom/workers/test/xpcshell/data/base_uri_module.mjs b/dom/workers/test/xpcshell/data/base_uri_module.mjs new file mode 100644 index 0000000000..7604baed82 --- /dev/null +++ b/dom/workers/test/xpcshell/data/base_uri_module.mjs @@ -0,0 +1,23 @@ +// This file is for testing the module loader's path handling. +// ESLint rules that modifies path shouldn't be applied. + +export const obj = {}; + +export async function doImport() { + // This file is loaded as resource://test/data/base_uri_module.mjs + // Relative/absolute paths should be resolved based on the URI, instead of + // file: path. + + const namespaceWithURI = await import( + "resource://test/data/base_uri_module2.mjs" + ); + const namespaceWithCurrentDir = await import("./base_uri_module2.mjs"); + const namespaceWithParentDir = await import("../data/base_uri_module2.mjs"); + const namespaceWithAbsoluteDir = await import("/data/base_uri_module2.mjs"); + + return { + equal1: namespaceWithURI.obj2 == namespaceWithCurrentDir.obj2, + equal2: namespaceWithURI.obj2 == namespaceWithParentDir.obj2, + equal3: namespaceWithURI.obj2 == namespaceWithAbsoluteDir.obj2, + }; +} diff --git a/dom/workers/test/xpcshell/data/base_uri_module2.mjs b/dom/workers/test/xpcshell/data/base_uri_module2.mjs new file mode 100644 index 0000000000..2358d27a83 --- /dev/null +++ b/dom/workers/test/xpcshell/data/base_uri_module2.mjs @@ -0,0 +1 @@ +export const obj2 = {}; diff --git a/dom/workers/test/xpcshell/data/base_uri_worker.js b/dom/workers/test/xpcshell/data/base_uri_worker.js new file mode 100644 index 0000000000..74137cc20b --- /dev/null +++ b/dom/workers/test/xpcshell/data/base_uri_worker.js @@ -0,0 +1,27 @@ +// This file is for testing the module loader's path handling. +// ESLint rules that modifies path shouldn't be applied. + +onmessage = async event => { + // This file is loaded as resource://test/data/base_uri_worker.js + // Relative/absolute paths should be resolved based on the URI, instead of + // file: path. + + const namespaceWithURI = await import( + "resource://test/data/base_uri_module.mjs" + ); + const namespaceWithCurrentDir = await import("./base_uri_module.mjs"); + const namespaceWithParentDir = await import("../data/base_uri_module.mjs"); + const namespaceWithAbsoluteDir = await import("/data/base_uri_module.mjs"); + + postMessage({ + scriptToModule: { + equal1: namespaceWithURI.obj == namespaceWithCurrentDir.obj, + equal2: namespaceWithURI.obj == namespaceWithParentDir.obj, + equal3: namespaceWithURI.obj == namespaceWithAbsoluteDir.obj, + }, + moduleToModuleURI: await namespaceWithURI.doImport(), + moduleToModuleCurrent: await namespaceWithCurrentDir.doImport(), + moduleToModuleParent: await namespaceWithParentDir.doImport(), + moduleToModuleAbsolute: await namespaceWithAbsoluteDir.doImport(), + }); +}; diff --git a/dom/workers/test/xpcshell/data/chrome.manifest b/dom/workers/test/xpcshell/data/chrome.manifest new file mode 100644 index 0000000000..611e81fd4e --- /dev/null +++ b/dom/workers/test/xpcshell/data/chrome.manifest @@ -0,0 +1 @@ +content workers ./ diff --git a/dom/workers/test/xpcshell/data/worker.js b/dom/workers/test/xpcshell/data/worker.js new file mode 100644 index 0000000000..0a455f51c3 --- /dev/null +++ b/dom/workers/test/xpcshell/data/worker.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +self.onmessage = function (msg) { + self.postMessage("OK"); +}; diff --git a/dom/workers/test/xpcshell/data/worker_fileReader.js b/dom/workers/test/xpcshell/data/worker_fileReader.js new file mode 100644 index 0000000000..44e7e6499b --- /dev/null +++ b/dom/workers/test/xpcshell/data/worker_fileReader.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +self.onmessage = function (msg) { + var fr = new FileReader(); + self.postMessage("OK"); +}; diff --git a/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js new file mode 100644 index 0000000000..3028e5d539 --- /dev/null +++ b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js @@ -0,0 +1,558 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { createHttpServer } = AddonTestUtils; + +// Force ServiceWorkerRegistrar to init by calling do_get_profile. +// (This has to be called before AddonTestUtils.init, because it does +// also call do_get_profile internally but it doesn't notify +// profile-after-change). +do_get_profile(true); + +AddonTestUtils.init(this); +ExtensionTestUtils.init(this); + +const server = createHttpServer({ hosts: ["localhost"] }); + +server.registerPathHandler("/page.html", (request, response) => { + info(`/page.html is being requested: ${JSON.stringify(request)}`); + response.write(`<!DOCTYPE html>`); +}); + +server.registerPathHandler("/sw.js", (request, response) => { + info(`/sw.js is being requested: ${JSON.stringify(request)}`); + response.setHeader("Content-Type", "application/javascript"); + response.write(` + dump('Executing http://localhost/sw.js\\n'); + importScripts('sw-imported.js'); + dump('Executed importScripts from http://localhost/sw.js\\n'); + `); +}); + +server.registerPathHandler("/sw-imported.js", (request, response) => { + info(`/sw-imported.js is being requested: ${JSON.stringify(request)}`); + response.setHeader("Content-Type", "application/javascript"); + response.write(` + dump('importScript loaded from http://localhost/sw-imported.js\\n'); + self.onmessage = evt => evt.ports[0].postMessage('original-imported-script'); + `); +}); + +Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true); +// Make sure this test file doesn't run with the legacy behavior by +// setting explicitly the expected default value. +Services.prefs.setBoolPref( + "extensions.filterResponseServiceWorkerScript.disabled", + false +); +Services.prefs.setBoolPref("extensions.dnr.enabled", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled"); + Services.prefs.clearUserPref( + "extensions.filterResponseServiceWorkerScript.disabled" + ); + Services.prefs.clearUserPref("extensions.dnr.enabled"); +}); + +// Helper function used to be sure to clear any data that a previous test case +// may have left (e.g. service worker registration, cached service worker +// scripts). +// +// NOTE: Given that xpcshell test are running isolated from each other (unlike +// mochitests), we can just clear every storage type supported by clear data +// (instead of cherry picking what we want to clear based on the test cases +// part of this test file). +async function ensureDataCleanup() { + info("Clear any service worker or data previous test cases may have left"); + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); +} + +// Note that the update algorithm (https://w3c.github.io/ServiceWorker/#update-algorithm) +// builds an "updatedResourceMap" as part of its check process. This means that only a +// single fetch will be performed for "sw-imported.js" as part of the update check and its +// resulting install invocation. The installation's call to importScripts when evaluated +// will load the script directly out of the Cache API. +function testSWUpdate(contentPage) { + return contentPage.spawn([], async () => { + const oldReg = await this.content.navigator.serviceWorker.ready; + const reg = await oldReg.update(); + const sw = reg.installing || reg.waiting || reg.active; + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + sw.postMessage("worker-message", [port2]); + }); + }); +} + +add_task(async function test_extension_invalid_sw_scripts_redirect_ignored() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["<all_urls>", "webRequest", "webRequestBlocking"], + // In this test task, the extension resource is not expected to be + // requested at all, so it does not really matter whether the file is + // listed in web_accessible_resources. Regardless, add it to make sure + // that any load failure is not caused by the lack of being listed here. + web_accessible_resources: ["sw-unexpected-redirect.js"], + }, + background() { + browser.webRequest.onBeforeRequest.addListener( + req => { + if (req.url == "http://localhost/sw.js") { + const filter = browser.webRequest.filterResponseData(req.requestId); + filter.ondata = event => filter.write(event.data); + filter.onstop = event => filter.disconnect(); + filter.onerror = () => { + browser.test.sendMessage( + "filter-response-error:mainscript", + filter.error + ); + }; + return { + redirectUrl: browser.runtime.getURL("sw-unexpected-redirect.js"), + }; + } + + if (req.url == "http://localhost/sw-imported.js") { + const filter = browser.webRequest.filterResponseData(req.requestId); + filter.ondata = event => filter.write(event.data); + filter.onstop = event => filter.disconnect(); + filter.onerror = () => { + browser.test.sendMessage( + "filter-response-error:importscript", + filter.error + ); + }; + return { redirectUrl: "about:blank" }; + } + + return {}; + }, + { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] }, + ["blocking"] + ); + }, + files: { + "sw-unexpected-redirect.js": ` + dump('main worker redirected to moz-extension://UUID/sw-unexpected-redirect.js\\n'); + self.onmessage = evt => evt.ports[0].postMessage('sw-unexpected-redirect'); + `, + }, + }); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + + function awaitConsoleMessage(regexp) { + return new Promise(resolve => { + Services.console.registerListener(function listener(message) { + if (regexp.test(message.message)) { + Services.console.unregisterListener(listener); + resolve(message); + } + }); + }); + } + + const awaitIgnoredMainScriptRedirect = awaitConsoleMessage( + /Invalid redirectUrl .* on service worker main script/ + ); + const awaitIgnoredImportScriptRedirect = awaitConsoleMessage( + /Invalid redirectUrl .* on service worker imported script/ + ); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + // Register the worker after loading the test extension, which should not be + // able to intercept and redirect the importedScripts requests because of + // invalid destinations. + info("Register service worker from a content webpage"); + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (importScripts not intercepted)" + ); + + info("Wait for the expected error message on main script redirect"); + const errorMsg = await awaitIgnoredMainScriptRedirect; + ok(errorMsg?.message, `Got error message: ${errorMsg?.message}`); + ok( + errorMsg?.message?.includes(extension.id), + "error message should include the addon id" + ); + ok( + errorMsg?.message?.includes("http://localhost/sw.js"), + "error message should include the sw main script url" + ); + + info("Wait for the expected error message on import script redirect"); + const errorMsg2 = await awaitIgnoredImportScriptRedirect; + ok(errorMsg2?.message, `Got error message: ${errorMsg2?.message}`); + ok( + errorMsg2?.message?.includes(extension.id), + "error message should include the addon id" + ); + ok( + errorMsg2?.message?.includes("http://localhost/sw-imported.js"), + "error message should include the sw main script url" + ); + + info("Wait filterResponse error on main script"); + equal( + await extension.awaitMessage("filter-response-error:mainscript"), + "Invalid request ID", + "Got expected error on main script" + ); + info("Wait filterResponse error on import script"); + equal( + await extension.awaitMessage("filter-response-error:importscript"), + "Invalid request ID", + "Got expected error on import script" + ); + + await extension.unload(); + await contentPage.close(); +}); + +add_task(async function test_filter_sw_script() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "<all_urls>", + "webRequest", + "webRequestBlocking", + "webRequestFilterResponse.serviceWorkerScript", + ], + }, + background() { + browser.webRequest.onBeforeRequest.addListener( + req => { + if (req.url == "http://localhost/sw.js") { + const filter = browser.webRequest.filterResponseData(req.requestId); + let decoder = new TextDecoder("utf-8"); + let encoder = new TextEncoder(); + filter.ondata = event => { + let str = decoder.decode(event.data, { stream: true }); + browser.test.log(`Got filter ondata event: ${str}\n`); + str = ` + dump('Executing filterResponse script for http://localhost/sw.js\\n'); + self.onmessage = evt => evt.ports[0].postMessage('filter-response-script'); + dump('Executed firlterResponse script for http://localhost/sw.js\\n'); + `; + filter.write(encoder.encode(str)); + filter.disconnect(); + }; + } + + return {}; + }, + { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] }, + ["blocking"] + ); + }, + }); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + + await ensureDataCleanup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "filter-response-script", + "Got expected worker reply (filterResponse script)" + ); + + await extension.unload(); + workerMessage = await testSWUpdate(contentPage); + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (original script)" + ); + + await contentPage.close(); +}); + +add_task(async function test_extension_redirect_sw_imported_script() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["<all_urls>", "webRequest", "webRequestBlocking"], + web_accessible_resources: ["sw-imported-1.js", "sw-imported-2.js"], + }, + background() { + let i = 1; + browser.webRequest.onBeforeRequest.addListener( + req => { + browser.test.log( + "Extension is redirecting http://localhost/sw-imported.js" + ); + browser.test.sendMessage("request-redirected"); + return { + redirectUrl: browser.runtime.getURL(`sw-imported-${i++}.js`), + }; + }, + { urls: ["http://localhost/sw-imported.js"] }, + ["blocking"] + ); + }, + files: { + "sw-imported-1.js": ` + dump('importScript redirected to moz-extension://UUID/sw-imported1.js \\n'); + self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-1'); + `, + "sw-imported-2.js": ` + dump('importScript redirected to moz-extension://UUID/sw-imported2.js \\n'); + self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-2'); + `, + }, + }); + + await ensureDataCleanup(); + const contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + // Register the worker while the test extension isn't loaded and cannot + // intercept and redirect the importedScripts requests. + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (importScripts not intercepted)" + ); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + + // Trigger an update on the registered service worker, then assert that the + // reply got is coming from the script where the extension is redirecting the + // request. + info("Update service worker and expect extension script to reply"); + workerMessage = await testSWUpdate(contentPage); + await extension.awaitMessage("request-redirected"); + equal( + workerMessage, + "redirected-imported-script-1", + "Got expected worker reply (importScripts redirected to moz-extension url)" + ); + + // Trigger a new update of the registered service worker, then assert that the + // reply got is coming from a different script where the extension is + // redirecting the second request (this confirms that the extension can + // intercept and can change the redirected imported scripts on new service + // worker updates). + info("Update service worker and expect new extension script to reply"); + workerMessage = await testSWUpdate(contentPage); + await extension.awaitMessage("request-redirected"); + equal( + workerMessage, + "redirected-imported-script-2", + "Got expected worker reply (importScripts redirected to moz-extension url again)" + ); + + // Uninstall the extension and trigger one more update of the registered + // service worker, then assert that the reply got is the one coming from the + // server (because difference from the one got from the cache). + // This verify that the service worker are updated as expected after the + // extension is uninstalled and the worker is not stuck on the script where + // the extension did redirect the request the last time. + info( + "Unload extension, update service worker and expect original script to reply" + ); + await extension.unload(); + workerMessage = await testSWUpdate(contentPage); + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (importScripts not intercepted)" + ); + + await contentPage.close(); +}); + +// Test cases for redirects with declarativeNetRequest instead of webRequest, +// testing the same scenarios as: +// - test_extension_invalid_sw_scripts_redirect_ignored +// i.e. fail to redirect SW main script, fail to redirect SW to about:blank. +// - test_extension_redirect_sw_imported_script +// i.e. allowed to redirect importScripts to moz-extension. +add_task(async function test_dnr_redirect_sw_script_or_import() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + permissions: ["declarativeNetRequest"], + host_permissions: ["<all_urls>"], + granted_host_permissions: true, + web_accessible_resources: [ + { + resources: ["sw-bad-redirect.js", "sw-dnr-redirect.js", "sw-nest.js"], + matches: ["*://*/*"], + }, + ], + }, + temporarilyInstalled: true, // <-- for granted_host_permissions + background: async () => { + await browser.declarativeNetRequest.updateSessionRules({ + addRules: [ + { + id: 1, + condition: { urlFilter: "|http://localhost/sw.js?dnr_redir_bad" }, + action: { + type: "redirect", + redirect: { extensionPath: "/sw-bad-redirect.js" }, + }, + }, + { + id: 2, + condition: { urlFilter: "|http://localhost/sw-imported.js|" }, + action: { + type: "redirect", + redirect: { extensionPath: "/sw-dnr-redirect.js" }, + }, + }, + { + id: 3, + condition: { urlFilter: "|http://localhost/sw-nest.js|" }, + action: { + type: "redirect", + redirect: { extensionPath: "/sw-nest.js" }, + }, + }, + { + id: 4, + condition: { urlFilter: "|http://localhost/sw-imported.js?about|" }, + action: { + type: "redirect", + redirect: { url: "about:blank" }, + }, + }, + ], + }); + browser.test.sendMessage("dnr_registered"); + }, + files: { + "sw-bad-redirect.js": String.raw` + dump('main worker redirected to moz-extension://UUID/sw-bad-redirect.js\n'); + self.onmessage = evt => evt.ports[0].postMessage('sw-bad-redirect'); + `, + "sw-dnr-redirect.js": String.raw` + dump('importScript redirected to moz-extension://UUID/sw-dnr-redirect.js\n'); + self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-before-nest'); + + importScripts("/sw-nest.js"); + // ^ sw-nest.js does not exist on the server, so if importScripts() + // succeeded, then that means that the DNR-triggered redirect worked. + + self.onmessage = evt => evt.ports[0].postMessage('sw-before-about'); + try { + importScripts("/sw-imported.js?about"); + // ^ DNR redirects to about:blank, which should throw here. + self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-about-bad'); + } catch (e) { + // All is good. + self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-redirect'); + } + `, + "sw-nest.js": String.raw` + dump('importScript redirected to moz-extension://UUID/sw-nest.js\n'); + // No other code here. The caller verifies success by confirming that + // the importScripts() call did not throw. + `, + }, + }); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + await extension.awaitMessage("dnr_registered"); + + await ensureDataCleanup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + // Register the worker after loading the test extension, which should not be + // able to intercept and redirect the importedScripts requests because of + // invalid destinations. + info("Register service worker from a content webpage (disallowed redirects)"); + await contentPage.spawn([], async () => { + await Assert.rejects( + this.content.navigator.serviceWorker.register("/sw.js?dnr_redir_bad1"), + /SecurityError: The operation is insecure/, + "Redirect of main service worker script is not allowed" + ); + }); + info("Register service worker from a content webpage (with import redirect)"); + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "sw-dnr-redirect", + "Got expected worker reply (importScripts redirected to moz-extension:-URL)" + ); + + await extension.unload(); + await contentPage.close(); +}); diff --git a/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js b/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js new file mode 100644 index 0000000000..718093422b --- /dev/null +++ b/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js @@ -0,0 +1,112 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { createHttpServer } = AddonTestUtils; + +// Force ServiceWorkerRegistrar to init by calling do_get_profile. +// (This has to be called before AddonTestUtils.init, because it does +// also call do_get_profile internally but it doesn't notify +// profile-after-change). +do_get_profile(true); + +AddonTestUtils.init(this); +ExtensionTestUtils.init(this); + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write("dummy page"); +}); + +add_setup(() => { + info("Making sure Services.io.offline is true"); + // Explicitly setting Services.io.offline to true makes this test able + // to hit on Desktop builds the same issue that test_ext_cache_api.js + // was hitting on Android builds (Bug 1844825). + Services.io.offline = true; +}); + +// Regression test derived from Bug 1844825. +add_task(async function test_fetch_request_from_ext_shared_worker() { + if (!WebExtensionPolicy.useRemoteWebExtensions) { + // Ensure RemoteWorkerService has been initialized in the main + // process. + Services.obs.notifyObservers(null, "profile-after-change"); + } + + const background = async function () { + const testUrl = `http://example.com/dummy`; + const worker = new SharedWorker("worker.js"); + const { data: result } = await new Promise(resolve => { + worker.port.onmessage = resolve; + worker.port.postMessage(["worker-fetch-test", testUrl]); + }); + + browser.test.sendMessage("test-sharedworker-fetch:done", result); + }; + + const extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { permissions: ["http://example.com/*"] }, + files: { + "worker.js": function () { + self.onconnect = evt => { + const port = evt.ports[0]; + port.onmessage = async evt => { + let result = {}; + let message; + try { + const [msg, url] = evt.data; + message = msg; + const response = await fetch(url); + dump(`fetch call resolved: ${response}\n`); + result.fetchResolvesTo = `${response}`; + } catch (err) { + dump(`fetch call rejected: ${err}\n`); + result.error = err.name; + throw err; + } finally { + port.postMessage([`${message}:result`, result]); + } + }; + }; + }, + }, + }); + + await extension.startup(); + const result = await extension.awaitMessage("test-sharedworker-fetch:done"); + if (Services.io.offline && WebExtensionPolicy.useRemoteWebExtensions) { + // If the network is offline and the extensions are running in the + // child extension process, expect the fetch call to be rejected + // with an TypeError. + Assert.deepEqual( + ["worker-fetch-test:result", { error: "TypeError" }], + result, + "fetch should have been rejected with an TypeError" + ); + } else { + // If the network is not offline or the extension are running in the + // parent process, we expect the fetch call to resolve to a Response. + Assert.deepEqual( + ["worker-fetch-test:result", { fetchResolvesTo: "[object Response]" }], + result, + "fetch should have been resolved to a Response instance" + ); + } + await extension.unload(); +}); diff --git a/dom/workers/test/xpcshell/test_fileReader.js b/dom/workers/test/xpcshell/test_fileReader.js new file mode 100644 index 0000000000..02bc3fa667 --- /dev/null +++ b/dom/workers/test/xpcshell/test_fileReader.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker_fileReader.js"; +do_load_manifest("data/chrome.manifest"); + +function talk_with_worker(worker) { + return new Promise((resolve, reject) => { + worker.onmessage = function (event) { + let success = true; + if (event.data == "OK") { + resolve(); + } else { + success = false; + reject(event); + } + Assert.ok(success); + worker.terminate(); + }; + worker.onerror = function (event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + reject(error); + }; + worker.postMessage("START"); + }); +} + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/test_import_base_uri.js b/dom/workers/test/xpcshell/test_import_base_uri.js new file mode 100644 index 0000000000..7c88a946f4 --- /dev/null +++ b/dom/workers/test/xpcshell/test_import_base_uri.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function testSyncImportBeforeAsyncImportDependencyInWorker() { + const worker = new ChromeWorker("resource://test/data/base_uri_worker.js"); + + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = event => { + resolve(event.data); + }; + worker.postMessage(""); + + const result = await promise; + + Assert.ok(result.scriptToModule.equal1); + Assert.ok(result.scriptToModule.equal2); + Assert.ok(result.scriptToModule.equal3); + + Assert.ok(result.moduleToModuleURI.equal1); + Assert.ok(result.moduleToModuleURI.equal2); + Assert.ok(result.moduleToModuleURI.equal3); + + Assert.ok(result.moduleToModuleCurrent.equal1); + Assert.ok(result.moduleToModuleCurrent.equal2); + Assert.ok(result.moduleToModuleCurrent.equal3); + + Assert.ok(result.moduleToModuleParent.equal1); + Assert.ok(result.moduleToModuleParent.equal2); + Assert.ok(result.moduleToModuleParent.equal3); + + Assert.ok(result.moduleToModuleAbsolute.equal1); + Assert.ok(result.moduleToModuleAbsolute.equal2); + Assert.ok(result.moduleToModuleAbsolute.equal3); +}); diff --git a/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js new file mode 100644 index 0000000000..b95ad4bcaf --- /dev/null +++ b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js @@ -0,0 +1,88 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { createHttpServer } = AddonTestUtils; + +// Force ServiceWorkerRegistrar to init by calling do_get_profile. +// (This has to be called before AddonTestUtils.init, because it does +// also call do_get_profile internally but it doesn't notify +// profile-after-change). +do_get_profile(true); + +AddonTestUtils.init(this); + +const server = createHttpServer({ hosts: ["localhost"] }); + +server.registerPathHandler("/sw.js", (request, response) => { + info(`/sw.js is being requested: ${JSON.stringify(request)}`); + response.setHeader("Content-Type", "application/javascript"); + response.write(""); +}); + +add_task(async function setup_prefs() { + // Enable nsIServiceWorkerManager.registerForTest. + Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled"); + }); +}); + +/** + * This test installs a ServiceWorker via test API and verify that the install + * process spawns a new process. (Normally ServiceWorker installation won't + * cause a new content process to be spawned because the call to register must + * be coming from within an existing content process, but the registerForTest + * API allows us to bypass this restriction.) + * + * This models the real-world situation of a push notification being received + * from the network which results in a ServiceWorker being spawned without their + * necessarily being an existing content process to host it (especially under Fission). + */ +add_task(async function launch_remoteworkers_in_new_processes() { + const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + const ssm = Services.scriptSecurityManager; + + const initialChildCount = Services.ppmm.childCount; + + // A test service worker that should spawn a regular web content child process. + const swRegInfoWeb = await swm.registerForTest( + ssm.createContentPrincipal(Services.io.newURI("http://localhost"), {}), + "http://localhost/scope", + "http://localhost/sw.js" + ); + swRegInfoWeb.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo); + + info( + `web content service worker registered: ${JSON.stringify({ + principal: swRegInfoWeb.principal.spec, + scope: swRegInfoWeb.scope, + })}` + ); + + info("Wait new process to be launched"); + await TestUtils.waitForCondition(() => { + return Services.ppmm.childCount - initialChildCount >= 1; + }, "wait for a new child processes to be started"); + + // Wait both workers to become active to be sure that. besides spawning + // the new child processes as expected, the two remote worker have been + // able to run successfully (in other word their remote worker data did + // pass successfull the IsRemoteTypeAllowed check in RemoteworkerChild). + info("Wait for webcontent worker to become active"); + await TestUtils.waitForCondition( + () => swRegInfoWeb.activeWorker, + `wait workers for scope ${swRegInfoWeb.scope} to be active` + ); +}); diff --git a/dom/workers/test/xpcshell/test_workers.js b/dom/workers/test/xpcshell/test_workers.js new file mode 100644 index 0000000000..5b768f69bb --- /dev/null +++ b/dom/workers/test/xpcshell/test_workers.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker.js"; +do_load_manifest("data/chrome.manifest"); + +function talk_with_worker(worker) { + return new Promise((resolve, reject) => { + worker.onmessage = function (event) { + let success = true; + if (event.data == "OK") { + resolve(); + } else { + success = false; + reject(event); + } + Assert.ok(success); + worker.terminate(); + }; + worker.onerror = function (event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + reject(error); + }; + worker.postMessage("START"); + }); +} + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); + +add_task(function test_worker() { + return talk_with_worker(new Worker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/test_workers_clone_error.js b/dom/workers/test/xpcshell/test_workers_clone_error.js new file mode 100644 index 0000000000..f3fd430457 --- /dev/null +++ b/dom/workers/test/xpcshell/test_workers_clone_error.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker.js"; +do_load_manifest("data/chrome.manifest"); + +function talk_with_worker(worker) { + return new Promise((resolve, reject) => { + worker.onmessage = function (event) { + let success = true; + if (event.data == "OK") { + resolve(); + } else { + success = false; + reject(event); + } + Assert.ok(success); + worker.terminate(); + }; + worker.onerror = function (event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + reject(error); + }; + + try { + // eslint-disable-next-line no-eval + eval("/"); + } catch (e) { + worker.postMessage(new ClonedErrorHolder(e)); + } + }); +} + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); + +add_task(function test_worker() { + return talk_with_worker(new Worker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/xpcshell.toml b/dom/workers/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..61b1eef0c2 --- /dev/null +++ b/dom/workers/test/xpcshell/xpcshell.toml @@ -0,0 +1,38 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = [ + "data/worker.js", + "data/worker_fileReader.js", + "data/chrome.manifest", + "data/base_uri_worker.js", + "data/base_uri_module.mjs", + "data/base_uri_module2.mjs", +] + +["test_ext_redirects_sw_scripts.js"] +# The following firefox-appdir make sure that ExtensionTestUtils.loadExtension +# will be able to successfully start the background page (it does fail without +# it because there wouldn't be a global.tabTracker implementation as we would +# expect in a real Firefox, Fenix or Thunderbird instance). +firefox-appdir = "browser" + +["test_ext_worker_offline_fetch.js"] +firefox-appdir = "browser" + +["test_fileReader.js"] + +["test_import_base_uri.js"] + +["test_remoteworker_launch_new_process.js"] +# The following firefox-appdir make sure that this xpcshell test will run +# with e10s enabled (which is needed to make sure that the test case is +# going to launch the expected new processes) +firefox-appdir = "browser" +# Disable plugin loading to make it rr able to record and replay this test. +prefs = ["plugin.disable=true"] +skip-if = ["socketprocess_networking"] # Bug 1759035 + + +["test_workers.js"] + +["test_workers_clone_error.js"] |