/* -*- 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 #include "js/CompilationAndEvaluation.h" #include "js/LocaleSensitive.h" #include "js/MemoryMetrics.h" #include "js/SourceText.h" #include "MessageEventRunnable.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs.h" #include "mozilla/dom/BlobURLProtocolHandler.h" #include "mozilla/dom/ClientManager.h" #include "mozilla/dom/ClientSource.h" #include "mozilla/dom/ClientState.h" #include "mozilla/dom/Console.h" #include "mozilla/dom/DOMTypes.h" #include "mozilla/dom/Event.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/nsCSPUtils.h" #include "mozilla/dom/Performance.h" #include "mozilla/dom/PerformanceStorageWorker.h" #include "mozilla/dom/PromiseDebugging.h" #include "mozilla/dom/RemoteWorkerChild.h" #include "mozilla/dom/WorkerBinding.h" #include "mozilla/ThreadEventQueue.h" #include "mozilla/ThrottledEventQueue.h" #include "mozilla/TimelineConsumers.h" #include "mozilla/WorkerTimelineMarker.h" #include "nsCycleCollector.h" #include "nsGlobalWindowInner.h" #include "nsNetUtil.h" #include "nsIMemoryReporter.h" #include "nsIPermissionManager.h" #include "nsIRandomGenerator.h" #include "nsIScriptError.h" #include "nsIScriptTimeoutHandler.h" #include "nsIURI.h" #include "nsIURL.h" #include "nsPrintfCString.h" #include "nsQueryObject.h" #include "nsRFPService.h" #include "nsSandboxFlags.h" #include "nsUTF8Utils.h" #include "RuntimeService.h" #include "ScriptLoader.h" #include "mozilla/dom/ServiceWorkerEvents.h" #include "mozilla/dom/ServiceWorkerManager.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 "WorkerScope.h" #include "WorkerThread.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 #define LOG(log, _args) MOZ_LOG(log, LogLevel::Debug, _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 AutoPtrComparator { typedef nsAutoPtr A; typedef T* B; public: bool Equals(const A& a, const B& b) const { return a && b ? *a == *b : !a && !b ? true : false; } bool LessThan(const A& a, const B& b) const { return a && b ? *a < *b : b ? true : false; } }; template inline AutoPtrComparator GetAutoPtrComparator(const nsTArray >&) { return AutoPtrComparator(); } // 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 mWrappedRunnable; public: ExternalRunnableWrapper(WorkerPrivate* aWorkerPrivate, nsIRunnable* aWrappedRunnable) : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), mWrappedRunnable(aWrappedRunnable) { MOZ_ASSERT(aWorkerPrivate); MOZ_ASSERT(aWrappedRunnable); } NS_INLINE_DECL_REFCOUNTING_INHERITED(ExternalRunnableWrapper, WorkerRunnable) private: ~ExternalRunnableWrapper() { } 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(); if (NS_FAILED(rv)) { if (!JS_IsExceptionPending(aCx)) { Throw(aCx, rv); } return false; } return true; } nsresult Cancel() override { nsresult rv; nsCOMPtr cancelable = do_QueryInterface(mWrappedRunnable); MOZ_ASSERT(cancelable); // We checked this earlier! rv = cancelable->Cancel(); nsresult rv2 = WorkerRunnable::Cancel(); return NS_FAILED(rv) ? rv : rv2; } }; 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, WorkerThreadUnchangedBusyCount), mFinishedWorker(aFinishedWorker) { } 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 { 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(); } NS_INLINE_DECL_REFCOUNTING_INHERITED(TopLevelWorkerFinishedRunnable, Runnable) private: ~TopLevelWorkerFinishedRunnable() {} NS_IMETHOD Run() override { AssertIsOnMainThread(); 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 ModifyBusyCountRunnable final : public WorkerControlRunnable { bool mIncrease; public: ModifyBusyCountRunnable(WorkerPrivate* aWorkerPrivate, bool aIncrease) : WorkerControlRunnable(aWorkerPrivate, ParentThreadUnchangedBusyCount), mIncrease(aIncrease) { } private: virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { return aWorkerPrivate->ModifyBusyCount(mIncrease); } virtual void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) override { if (mIncrease) { WorkerControlRunnable::PostRun(aCx, aWorkerPrivate, aRunResult); return; } // Don't do anything here as it's possible that aWorkerPrivate has been // deleted. } }; class CompileScriptRunnable final : public WorkerRunnable { nsString mScriptURL; public: explicit CompileScriptRunnable(WorkerPrivate* aWorkerPrivate, const nsAString& aScriptURL) : WorkerRunnable(aWorkerPrivate), mScriptURL(aScriptURL) { } 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(); if (NS_WARN_IF(!aWorkerPrivate->EnsureClientSource())) { return false; } if (NS_WARN_IF(!aWorkerPrivate->EnsureCSPEventListener())) { return false; } // PerformanceStorage & PerformanceCounter both need to be initialized // on the worker thread before being used on main-thread. // Let's be sure that it is created before any // content loading. aWorkerPrivate->EnsurePerformanceStorage(); if (mozilla::StaticPrefs::dom_performance_enable_scheduler_timing()) { aWorkerPrivate->EnsurePerformanceCounter(); } ErrorResult rv; workerinternals::LoadMainScript(aWorkerPrivate, mScriptURL, WorkerScript, rv); 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; } WorkerGlobalScope* globalScope = aWorkerPrivate->GlobalScope(); if (NS_WARN_IF(!globalScope)) { // We never got as far as calling GetOrCreateGlobalScope, or it failed. // We have no way to enter a compartment, hence no sane way to report this // error. :( 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)) { return false; } 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, WorkerThreadUnchangedBusyCount), mStatus(aStatus) { MOZ_ASSERT(aStatus == Closing || aStatus == Canceling || aStatus == Killing); } private: virtual bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->AssertIsOnParentThread(); return aWorkerPrivate->ModifyBusyCount(true); } virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override { aWorkerPrivate->AssertIsOnParentThread(); if (!aDispatchResult) { // We couldn't dispatch to the worker, which means it's already dead. // Undo the busy count modification. aWorkerPrivate->ModifyBusyCount(false); } } virtual void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) override { aWorkerPrivate->ModifyBusyCountFromWorker(false); } virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { return aWorkerPrivate->NotifyInternal(mStatus); } }; class FreezeRunnable final : public WorkerControlRunnable { public: explicit FreezeRunnable(WorkerPrivate* aWorkerPrivate) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) { } private: virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { return aWorkerPrivate->FreezeInternal(); } }; class ThawRunnable final : public WorkerControlRunnable { public: explicit ThawRunnable(WorkerPrivate* aWorkerPrivate) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) { } private: virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { return aWorkerPrivate->ThawInternal(); } }; class PropagateFirstPartyStorageAccessGrantedRunnable final : public WorkerControlRunnable { public: explicit PropagateFirstPartyStorageAccessGrantedRunnable(WorkerPrivate* aWorkerPrivate) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) {} private: bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->PropagateFirstPartyStorageAccessGrantedInternal(); return true; } }; class ReportErrorToConsoleRunnable final : public WorkerRunnable { const char* mMessage; const nsTArray 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& 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 runnable = new ReportErrorToConsoleRunnable(aWorkerPrivate, aMessage, aParams); runnable->Dispatch(); return; } uint16_t paramCount = aParams.Length(); const char16_t** params = new const char16_t*[paramCount]; for (uint16_t i=0; i& aParams) : WorkerRunnable(aWorkerPrivate, ParentThreadUnchangedBusyCount), mMessage(aMessage), mParams(aParams) { } 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 TimerRunnable final : public WorkerRunnable, public nsITimerCallback, public nsINamed { public: NS_DECL_ISUPPORTS_INHERITED explicit TimerRunnable(WorkerPrivate* aWorkerPrivate) : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) { } private: ~TimerRunnable() {} 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 { return aWorkerPrivate->RunExpiredTimeouts(aCx); } NS_IMETHOD Notify(nsITimer* aTimer) override { return Run(); } NS_IMETHOD GetName(nsACString& aName) override { aName.AssignLiteral("TimerRunnable"); return NS_OK; } }; NS_IMPL_ISUPPORTS_INHERITED(TimerRunnable, WorkerRunnable, nsITimerCallback, nsINamed) class DebuggerImmediateRunnable : public WorkerRunnable { RefPtr mHandler; public: explicit DebuggerImmediateRunnable(WorkerPrivate* aWorkerPrivate, dom::Function& aHandler) : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), 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 global(aCx, JS::CurrentGlobalOrNull(aCx)); JS::Rooted callable(aCx, JS::ObjectOrNullValue(mHandler->CallableOrNull())); JS::HandleValueArray args = JS::HandleValueArray::empty(); JS::Rooted rval(aCx); if (!JS_CallFunctionValue(aCx, global, callable, args, &rval)) { // Just return false; WorkerRunnable::Run will report the exception. return false; } return true; } }; void PeriodicGCTimerCallback(nsITimer* aTimer, void* aClosure) { auto workerPrivate = static_cast(aClosure); MOZ_DIAGNOSTIC_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); workerPrivate->GarbageCollectInternal(workerPrivate->GetJSContext(), false /* shrinking */, false /* collect children */); } void IdleGCTimerCallback(nsITimer* aTimer, void* aClosure) { auto workerPrivate = static_cast(aClosure); MOZ_DIAGNOSTIC_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); workerPrivate->GarbageCollectInternal(workerPrivate->GetJSContext(), true /* shrinking */, false /* collect children */); } class UpdateContextOptionsRunnable final : public WorkerControlRunnable { JS::ContextOptions mContextOptions; public: UpdateContextOptionsRunnable(WorkerPrivate* aWorkerPrivate, const JS::ContextOptions& aContextOptions) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), mContextOptions(aContextOptions) { } private: virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->UpdateContextOptionsInternal(aCx, mContextOptions); return true; } }; class UpdateLanguagesRunnable final : public WorkerRunnable { nsTArray mLanguages; public: UpdateLanguagesRunnable(WorkerPrivate* aWorkerPrivate, const nsTArray& aLanguages) : WorkerRunnable(aWorkerPrivate), mLanguages(aLanguages) { } virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->UpdateLanguagesInternal(mLanguages); return true; } }; class UpdateJSWorkerMemoryParameterRunnable final : public WorkerControlRunnable { uint32_t mValue; JSGCParamKey mKey; public: UpdateJSWorkerMemoryParameterRunnable(WorkerPrivate* aWorkerPrivate, JSGCParamKey aKey, uint32_t aValue) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), 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, WorkerThreadUnchangedBusyCount), mGCZeal(aGCZeal), mFrequency(aFrequency) { } private: virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->UpdateGCZealInternal(aCx, mGCZeal, mFrequency); return true; } }; #endif class GarbageCollectRunnable final : public WorkerControlRunnable { bool mShrinking; bool mCollectChildren; public: GarbageCollectRunnable(WorkerPrivate* aWorkerPrivate, bool aShrinking, bool aCollectChildren) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), 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); return true; } }; class CycleCollectRunnable : public WorkerControlRunnable { bool mCollectChildren; public: CycleCollectRunnable(WorkerPrivate* aWorkerPrivate, bool aCollectChildren) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), mCollectChildren(aCollectChildren) { } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->CycleCollectInternal(mCollectChildren); return true; } }; class OfflineStatusChangeRunnable : public WorkerRunnable { public: OfflineStatusChangeRunnable(WorkerPrivate* aWorkerPrivate, bool aIsOffline) : WorkerRunnable(aWorkerPrivate), mIsOffline(aIsOffline) { } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->OfflineStatusChangeEventInternal(mIsOffline); return true; } private: bool mIsOffline; }; class MemoryPressureRunnable : public WorkerControlRunnable { public: explicit MemoryPressureRunnable(WorkerPrivate* aWorkerPrivate) : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) {} bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->MemoryPressureInternal(); return true; } }; #ifdef DEBUG static bool StartsWithExplicit(nsACString& s) { return StringBeginsWith(s, NS_LITERAL_CSTRING("explicit/")); } #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, ParentThreadUnchangedBusyCount) {} 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, ParentThreadUnchangedBusyCount) {} 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 { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); // Now we can cancel the this worker from the parent process. RefPtr r = new CancelingOnParentRunnable(workerPrivate); r->Dispatch(); return NS_OK; } }; } /* anonymous namespace */ 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; nsIEventTarget* mWeakNestedEventTarget; nsCOMPtr mNestedEventTarget; public: explicit EventTarget(WorkerPrivate* aWorkerPrivate) : mMutex("WorkerPrivate::EventTarget::mMutex"), mWorkerPrivate(aWorkerPrivate), mWeakNestedEventTarget(nullptr) { MOZ_ASSERT(aWorkerPrivate); } EventTarget(WorkerPrivate* aWorkerPrivate, nsIEventTarget* aNestedEventTarget) : mMutex("WorkerPrivate::EventTarget::mMutex"), mWorkerPrivate(aWorkerPrivate), mWeakNestedEventTarget(aNestedEventTarget), mNestedEventTarget(aNestedEventTarget) { MOZ_ASSERT(aWorkerPrivate); MOZ_ASSERT(aNestedEventTarget); } void Disable() { nsCOMPtr nestedEventTarget; { MutexAutoLock lock(mMutex); // Note, Disable() can be called more than once safely. mWorkerPrivate = nullptr; mNestedEventTarget.swap(nestedEventTarget); } } nsIEventTarget* GetWeakNestedEventTarget() const { MOZ_ASSERT(mWeakNestedEventTarget); return mWeakNestedEventTarget; } NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIEVENTTARGET_FULL private: ~EventTarget() { } }; struct WorkerPrivate::TimeoutInfo { TimeoutInfo() : mId(0), mIsInterval(false), mCanceled(false) { MOZ_COUNT_CTOR(mozilla::dom::WorkerPrivate::TimeoutInfo); } ~TimeoutInfo() { MOZ_COUNT_DTOR(mozilla::dom::WorkerPrivate::TimeoutInfo); } bool operator==(const TimeoutInfo& aOther) { return mTargetTime == aOther.mTargetTime; } bool operator<(const TimeoutInfo& aOther) { return mTargetTime < aOther.mTargetTime; } nsCOMPtr mHandler; mozilla::TimeStamp mTargetTime; mozilla::TimeDuration mInterval; int32_t mId; bool mIsInterval; bool mCanceled; }; class WorkerJSContextStats final : public JS::RuntimeStats { const nsCString mRtPath; public: explicit WorkerJSContextStats(const nsACString& aRtPath) : JS::RuntimeStats(JsWorkerMallocSizeOf), mRtPath(aRtPath) { } ~WorkerJSContextStats() { for (size_t i = 0; i != zoneStatsVector.length(); i++) { delete static_cast(zoneStatsVector[i].extra); } for (size_t i = 0; i != realmStatsVector.length(); i++) { delete static_cast(realmStatsVector[i].extra); } } const nsCString& Path() const { return mRtPath; } virtual void initExtraZoneStats(JS::Zone* aZone, JS::ZoneStats* aZoneStats) 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::Handle aRealm, JS::RealmStats* aRealmStats) 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 += NS_LITERAL_CSTRING("realm(web-worker)/"); // 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 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->DispatchToMainThread(mFinishCollectRunnable.forget())); } }; class FinishCollectRunnable final : public Runnable { nsCOMPtr mHandleReport; nsCOMPtr 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; } private: ~FinishCollectRunnable() { // mHandleReport and mHandlerData are released on the main thread. AssertIsOnMainThread(); } FinishCollectRunnable(const FinishCollectRunnable&) = delete; FinishCollectRunnable& operator=(const FinishCollectRunnable&) = delete; FinishCollectRunnable& operator=(const FinishCollectRunnable&&) = delete; }; ~MemoryReporter() { } 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 runnable; { MutexAutoLock lock(mMutex); if (!mWorkerPrivate) { // This will effectively report 0 memory. nsCOMPtr 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(")/worker("); } 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(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 scope = aWorkerPrivate->GlobalScope(); RefPtr 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 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(EmptyCString(), path, nsIMemoryReporter::KIND_HEAP, nsIMemoryReporter::UNITS_BYTES, mPerformanceUserEntries, NS_LITERAL_CSTRING("Memory used for performance user entries."), mHandlerData); } if (mPerformanceResourceEntries) { nsCString path = mCxStats.Path(); path.AppendLiteral("dom/performance/resource-entries"); mHandleReport->Callback(EmptyCString(), path, nsIMemoryReporter::KIND_HEAP, nsIMemoryReporter::UNITS_BYTES, mPerformanceResourceEntries, NS_LITERAL_CSTRING("Memory used for performance resource entries."), mHandlerData); } } manager->EndReport(); return NS_OK; } WorkerPrivate::SyncLoopInfo::SyncLoopInfo(EventTarget* aEventTarget) : mEventTarget(aEventTarget), mCompleted(false), mResult(false) #ifdef DEBUG , mHasRun(false) #endif { } nsIDocument* WorkerPrivate::GetDocument() const { AssertIsOnMainThread(); if (mLoadInfo.mWindow) { return mLoadInfo.mWindow->GetExtantDoc(); } // 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->GetExtantDoc(); } parent = parent->GetParent(); } // couldn't query a document, give up and return nullptr return nullptr; } void WorkerPrivate::SetCSP(nsIContentSecurityPolicy* aCSP) { AssertIsOnMainThread(); if (!aCSP) { return; } aCSP->EnsureEventTarget(mMainThreadEventTarget); mLoadInfo.mCSP = aCSP; } nsresult WorkerPrivate::SetCSPFromHeaderValues(const nsACString& aCSPHeaderValue, const nsACString& aCSPReportOnlyHeaderValue) { AssertIsOnMainThread(); MOZ_DIAGNOSTIC_ASSERT(!mLoadInfo.mCSP); NS_ConvertASCIItoUTF16 cspHeaderValue(aCSPHeaderValue); NS_ConvertASCIItoUTF16 cspROHeaderValue(aCSPReportOnlyHeaderValue); nsCOMPtr csp; nsresult rv = mLoadInfo.mPrincipal->EnsureCSP(nullptr, getter_AddRefs(csp)); if (!csp) { return NS_OK; } 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); } // 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.mCSP = csp; mLoadInfo.mEvalAllowed = evalAllowed; mLoadInfo.mReportCSPViolations = reportEvalViolations; return NS_OK; } void WorkerPrivate::SetReferrerPolicyFromHeaderValue(const nsACString& aReferrerPolicyHeaderValue) { NS_ConvertUTF8toUTF16 headerValue(aReferrerPolicyHeaderValue); if (headerValue.IsEmpty()) { return; } net::ReferrerPolicy policy = nsContentUtils::GetReferrerPolicyFromHeader(headerValue); if (policy == net::RP_Unset) { return; } SetReferrerPolicy(policy); } 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 our busy count is zero 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 (!mBusyCount && !mMainThreadObjectsForgotten) { nsCycleCollectionTraversalCallback& cb = aCb; WorkerPrivate* tmp = this; NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParentEventTargetRef); } } nsresult WorkerPrivate::Dispatch(already_AddRefed aRunnable, nsIEventTarget* aSyncLoopTarget) { // May be called on any thread! RefPtr runnable(aRunnable); { MutexAutoLock lock(mMutex); MOZ_ASSERT_IF(aSyncLoopTarget, mThread); if (!mThread) { if (ParentStatus() == Pending || mStatus == Pending) { mPreStartRunnables.AppendElement(runnable); return NS_OK; } NS_WARNING("Using a worker event target after the thread has already" "been released!"); return NS_ERROR_UNEXPECTED; } if (mStatus == Dead || (!aSyncLoopTarget && ParentStatus() > Running)) { NS_WARNING("A runnable was posted to a worker that is already shutting " "down!"); return NS_ERROR_UNEXPECTED; } nsresult rv; if (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. 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(); if (NS_FAILED(UnregisterWorkerDebugger(this))) { NS_WARNING("Failed to unregister worker debugger!"); } } nsresult WorkerPrivate::DispatchControlRunnable(already_AddRefed aWorkerControlRunnable) { // May be called on any thread! RefPtr runnable(aWorkerControlRunnable); MOZ_ASSERT(runnable); { 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 aDebuggerRunnable) { // May be called on any thread! RefPtr 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 WorkerPrivate::MaybeWrapAsWorkerRunnable(already_AddRefed aRunnable) { // May be called on any thread! nsCOMPtr runnable(aRunnable); MOZ_ASSERT(runnable); RefPtr workerRunnable = WorkerRunnable::FromRunnable(runnable); if (workerRunnable) { return workerRunnable.forget(); } nsCOMPtr cancelable = do_QueryInterface(runnable); if (!cancelable) { MOZ_CRASH("All runnables destined for a worker thread must be cancelable!"); } workerRunnable = new ExternalRunnableWrapper(this, runnable); return workerRunnable.forget(); } bool WorkerPrivate::Start() { // May be called on any thread! { 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(); bool pending; { MutexAutoLock lock(mMutex); if (mParentStatus >= aStatus) { return true; } pending = mParentStatus == Pending; mParentStatus = aStatus; } 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; } RefPtr runnable = new NotifyRunnable(this, aStatus); return runnable->Dispatch(); } bool WorkerPrivate::Freeze(nsPIDOMWindowInner* aWindow) { AssertIsOnParentThread(); mParentFrozen = true; // 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(true)); } } { MutexAutoLock lock(mMutex); if (mParentStatus >= Canceling) { return true; } } DisableDebugger(); RefPtr runnable = new FreezeRunnable(this); if (!runnable->Dispatch()) { return false; } return true; } bool WorkerPrivate::Thaw(nsPIDOMWindowInner* aWindow) { AssertIsOnParentThread(); MOZ_ASSERT(mParentFrozen); mParentFrozen = false; // 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()); } { MutexAutoLock lock(mMutex); if (mParentStatus >= Canceling) { return true; } } EnableDebugger(); RefPtr runnable = new ThawRunnable(this); if (!runnable->Dispatch()) { return false; } return true; } 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) { // Pausing a ThrottledEventQueue is infallible. MOZ_ALWAYS_SUCCEEDS(mMainThreadDebuggeeEventTarget->SetIsPaused(true)); } } void WorkerPrivate::ParentWindowResumed() { AssertIsOnMainThread(); MOZ_ASSERT(mParentWindowPaused); mParentWindowPaused = false; { MutexAutoLock lock(mMutex); if (mParentStatus >= Canceling) { return; } } // 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()); } void WorkerPrivate::PropagateFirstPartyStorageAccessGranted() { AssertIsOnParentThread(); { MutexAutoLock lock(mMutex); if (mParentStatus >= Canceling) { return; } } RefPtr runnable = new PropagateFirstPartyStorageAccessGrantedRunnable(this); Unused << NS_WARN_IF(!runnable->Dispatch()); } bool WorkerPrivate::Close() { mMutex.AssertCurrentThreadOwns(); if (mParentStatus < Closing) { mParentStatus = Closing; } return true; } bool WorkerPrivate::ModifyBusyCount(bool aIncrease) { AssertIsOnParentThread(); MOZ_ASSERT(aIncrease || mBusyCount, "Mismatched busy count mods!"); if (aIncrease) { mBusyCount++; return true; } if (--mBusyCount == 0) { bool shouldCancel; { MutexAutoLock lock(mMutex); shouldCancel = mParentStatus == Canceling; } if (shouldCancel && !Cancel()) { return false; } } return true; } bool WorkerPrivate::ProxyReleaseMainThreadObjects() { AssertIsOnParentThread(); MOZ_ASSERT(!mMainThreadObjectsForgotten); nsCOMPtr 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, loadGroupToCancel); mMainThreadObjectsForgotten = true; return result; } void WorkerPrivate::UpdateContextOptions(const JS::ContextOptions& aContextOptions) { AssertIsOnParentThread(); { MutexAutoLock lock(mMutex); mJSSettings.contextOptions = aContextOptions; } RefPtr runnable = new UpdateContextOptionsRunnable(this, aContextOptions); if (!runnable->Dispatch()) { NS_WARNING("Failed to update worker context options!"); } } void WorkerPrivate::UpdateLanguages(const nsTArray& aLanguages) { AssertIsOnParentThread(); RefPtr runnable = new UpdateLanguagesRunnable(this, aLanguages); if (!runnable->Dispatch()) { NS_WARNING("Failed to update worker languages!"); } } void WorkerPrivate::UpdateJSWorkerMemoryParameter(JSGCParamKey aKey, uint32_t aValue) { AssertIsOnParentThread(); bool found = false; { MutexAutoLock lock(mMutex); found = mJSSettings.ApplyGCSetting(aKey, aValue); } if (found) { RefPtr 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 runnable = new UpdateGCZealRunnable(this, aGCZeal, aFrequency); if (!runnable->Dispatch()) { NS_WARNING("Failed to update worker gczeal!"); } } #endif void WorkerPrivate::GarbageCollect(bool aShrinking) { AssertIsOnParentThread(); RefPtr runnable = new GarbageCollectRunnable(this, aShrinking, /* collectChildren = */ true); if (!runnable->Dispatch()) { NS_WARNING("Failed to GC worker!"); } } void WorkerPrivate::CycleCollect(bool aDummy) { AssertIsOnParentThread(); RefPtr runnable = new CycleCollectRunnable(this, /* collectChildren = */ true); if (!runnable->Dispatch()) { NS_WARNING("Failed to CC worker!"); } } void WorkerPrivate::OfflineStatusChangeEvent(bool aIsOffline) { AssertIsOnParentThread(); RefPtr runnable = new OfflineStatusChangeRunnable(this, aIsOffline); if (!runnable->Dispatch()) { NS_WARNING("Failed to dispatch offline status change event!"); } } void WorkerPrivate::OfflineStatusChangeEventInternal(bool aIsOffline) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); // 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 nav = globalScope->GetExistingNavigator(); if (nav) { nav->SetOnLine(data->mOnLine); } nsString eventType; if (aIsOffline) { eventType.AssignLiteral("offline"); } else { eventType.AssignLiteral("online"); } RefPtr event = NS_NewDOMEvent(globalScope, nullptr, nullptr); event->InitEvent(eventType, false, false); event->SetTrusted(true); globalScope->DispatchEvent(*event); } void WorkerPrivate::MemoryPressure(bool aDummy) { AssertIsOnParentThread(); RefPtr runnable = new MemoryPressureRunnable(this); Unused << NS_WARN_IF(!runnable->Dispatch()); } 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 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::GetUTFOrigin(aBaseURI, mLocationInfo.mOrigin); } nsresult WorkerPrivate::SetPrincipalOnMainThread(nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup) { return mLoadInfo.SetPrincipalOnMainThread(aPrincipal, aLoadGroup); } nsresult WorkerPrivate::SetPrincipalFromChannel(nsIChannel* aChannel) { return mLoadInfo.SetPrincipalFromChannel(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->MaybeAddTabChild(aBaseLoadGroup); } #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) : mNumHoldersPreventingShutdownStart(0) , mDebuggerEventLoopLevel(0) , mErrorHandlerRecursionCount(0) , mNextTimeoutId(1) , mFrozen(false) , mTimerRunning(false) , mRunningExpiredTimeouts(false) , mPeriodicGCTimerRunning(false) , mIdleGCTimerRunning(false) , mOnLine(aParent ? aParent->OnLine() : !NS_IsOffline()) {} namespace { bool IsNewWorkerSecureContext(const WorkerPrivate* const aParent, const WorkerType aWorkerType, const WorkerLoadInfo& aLoadInfo) { if (aParent) { return aParent->IsSecureContext(); } // Our secure context state depends on the kind of worker we have. if (aLoadInfo.mPrincipalIsSystem) { return true; } if (aWorkerType == WorkerTypeService) { 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; } } WorkerPrivate::WorkerPrivate(WorkerPrivate* aParent, const nsAString& aScriptURL, bool aIsChromeWorker, WorkerType aWorkerType, const nsAString& aWorkerName, const nsACString& aServiceWorkerScope, WorkerLoadInfo& aLoadInfo) : mMutex("WorkerPrivate Mutex") , mCondVar(mMutex, "WorkerPrivate CondVar") , mParent(aParent) , mScriptURL(aScriptURL) , mWorkerName(aWorkerName) , mWorkerType(aWorkerType) , 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) , mBusyCount(0) , mLoadingWorkerScript(false) , mCreationTimeStamp(TimeStamp::Now()) , mCreationTimeHighRes((double)PR_Now() / PR_USEC_PER_MSEC) , mWorkerThreadAccessible(aParent) , mParentWindowPaused(false) , mPendingEventQueueClearing(false) , mCancelAllPendingRunnables(false) , mWorkerScriptExecutedSuccessfully(false) , mFetchHandlerWasAdded(false) , mMainThreadObjectsForgotten(false) , mIsChromeWorker(aIsChromeWorker) , mParentFrozen(false) , mIsSecureContext(IsNewWorkerSecureContext(mParent, mWorkerType, mLoadInfo)) , mDebuggerRegistered(false) , mIsInAutomation(false) , mPerformanceCounter(nullptr) { 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); } } else { AssertIsOnMainThread(); RuntimeService::GetDefaultJSSettings(mJSSettings); if (mIsSecureContext) { mJSSettings.chrome.realmOptions .creationOptions().setSecureContext(true); mJSSettings.chrome.realmOptions .creationOptions().setClampAndJitterTime(false); mJSSettings.content.realmOptions .creationOptions().setSecureContext(true); mJSSettings.content.realmOptions .creationOptions().setClampAndJitterTime(false); } 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 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) { mMainThreadEventTarget = aParent->mMainThreadEventTarget; mMainThreadDebuggeeEventTarget = aParent->mMainThreadDebuggeeEventTarget; return; } MOZ_ASSERT(NS_IsMainThread()); target = GetWindow() ? GetWindow()->EventTargetFor(TaskCategory::Worker) : 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. mMainThreadEventTarget = ThrottledEventQueue::Create(target); mMainThreadDebuggeeEventTarget = ThrottledEventQueue::Create(target); if (IsParentWindowPaused() || IsFrozen()) { MOZ_ALWAYS_SUCCEEDS(mMainThreadDebuggeeEventTarget->SetIsPaused(true)); } } WorkerPrivate::~WorkerPrivate() { DropJSObjects(this); 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); } // static already_AddRefed WorkerPrivate::Constructor(JSContext* aCx, const nsAString& aScriptURL, bool aIsChromeWorker, WorkerType aWorkerType, const nsAString& aWorkerName, const nsACString& aServiceWorkerScope, WorkerLoadInfo* aLoadInfo, ErrorResult& aRv) { 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 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 stackLoadInfo; if (!aLoadInfo) { stackLoadInfo.emplace(); nsresult rv = GetLoadInfo(aCx, nullptr, parent, aScriptURL, aIsChromeWorker, InheritLoadGroup, aWorkerType, 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); RefPtr worker = new WorkerPrivate(parent, aScriptURL, aIsChromeWorker, aWorkerType, aWorkerName, aServiceWorkerScope, *aLoadInfo); // 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; } worker->EnableDebugger(); MOZ_DIAGNOSTIC_ASSERT(worker->PrincipalIsValid()); RefPtr compiler = new CompileScriptRunnable(worker, aScriptURL); if (!compiler->Dispatch()) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } worker->mSelfRef = worker; return worker.forget(); } // static nsresult WorkerPrivate::GetLoadInfo(JSContext* aCx, nsPIDOMWindowInner* aWindow, WorkerPrivate* aParent, const nsAString& aScriptURL, bool aIsChromeWorker, LoadGroupBehavior aLoadGroupBehavior, WorkerType aWorkerType, 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, 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.mDomain = aParent->Domain(); loadInfo.mFromWindow = aParent->IsFromWindow(); loadInfo.mWindowID = aParent->WindowID(); loadInfo.mStorageAllowed = aParent->IsStorageAllowed(); loadInfo.mOriginAttributes = aParent->GetOriginAttributes(); loadInfo.mServiceWorkersTestingInWindow = aParent->ServiceWorkersTestingInWindow(); loadInfo.mParentController = aParent->GetController(); } 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); loadInfo.mPrincipalIsSystem = true; } // See if we're being called from a window. nsCOMPtr globalWindow = aWindow; if (!globalWindow) { globalWindow = xpc::CurrentWindowOrNull(aCx); } nsCOMPtr document; Maybe 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(); // TODO: fix this for SharedWorkers with multiple documents (bug 1177935) loadInfo.mServiceWorkersTestingInWindow = outerWindow->GetServiceWorkersTestingEnabled(); } if (!loadInfo.mWindow || (globalWindow != loadInfo.mWindow && !nsContentUtils::CanCallerAccess(loadInfo.mWindow))) { return NS_ERROR_DOM_SECURITY_ERR; } nsCOMPtr 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 tmpDoc = document; do { tmpDoc = tmpDoc->GetParentDocument(); } while (tmpDoc && tmpDoc->GetSandboxFlags() & SANDBOXED_ORIGIN); if (tmpDoc) { // There was an unsandboxed ancestor, yay! nsCOMPtr 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 permMgr = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); uint32_t perm; rv = permMgr->TestPermissionFromPrincipal(loadInfo.mLoadingPrincipal, "systemXHR", &perm); NS_ENSURE_SUCCESS(rv, rv); loadInfo.mXHRParamsAllowed = perm == nsIPermissionManager::ALLOW_ACTION; loadInfo.mFromWindow = true; loadInfo.mWindowID = globalWindow->WindowID(); nsContentUtils::StorageAccess access = nsContentUtils::StorageAllowedForWindow(globalWindow); loadInfo.mStorageAllowed = access > nsContentUtils::StorageAccess::eDeny; loadInfo.mOriginAttributes = nsContentUtils::GetOriginAttributes(document); 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 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.mStorageAllowed = true; loadInfo.mOriginAttributes = OriginAttributes(); } 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 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, clientInfo, ContentPolicyType(aWorkerType), getter_AddRefs(loadInfo.mChannel)); NS_ENSURE_SUCCESS(rv, rv); rv = NS_GetFinalChannelURI(loadInfo.mChannel, getter_AddRefs(loadInfo.mResolvedScriptURI)); NS_ENSURE_SUCCESS(rv, rv); rv = loadInfo.SetPrincipalFromChannel(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->MaybeAddTabChild(aLoadInfo.mLoadGroup); // NOTE: this defaults the load context to: // - private browsing = false // - content = true // - use remote tabs = false nsCOMPtr loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); nsresult rv = loadGroup->SetNotificationCallbacks(aLoadInfo.mInterfaceRequestor); MOZ_ALWAYS_SUCCEEDS(rv); aLoadInfo.mLoadGroup = loadGroup.forget(); MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(aLoadInfo.mLoadGroup, aPrincipal)); } void WorkerPrivate::DoRunLoop(JSContext* aCx) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_ASSERT(mThread); { MutexAutoLock lock(mMutex); mJSContext = aCx; 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(); for (;;) { WorkerStatus currentStatus; bool debuggerRunnablesPending = false; bool normalRunnablesPending = false; { MutexAutoLock lock(mMutex); while (mControlQueue.IsEmpty() && !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) && !(normalRunnablesPending = NS_HasPendingEvents(mThread))) { WaitForWorkerEvents(); } auto result = ProcessAllControlRunnablesLocked(); if (result != ProcessAllControlRunnablesResult::Nothing) { // NB: There's no JS on the stack here, so Abort vs MayContinue is // irrelevant // The state of the world may have changed, recheck it. normalRunnablesPending = NS_HasPendingEvents(mThread); // The debugger queue doesn't get cleared, so we can ignore that. } currentStatus = mStatus; } // if all holders are done then we can kill this thread. if (currentStatus != Running && !HasActiveHolders()) { // 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) { // The ClientSource should be cleared in NotifyInternal() when we reach // or pass Canceling. MOZ_DIAGNOSTIC_ASSERT(!data->mClientSource); // 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()) { WorkerControlRunnable* runnable = nullptr; while (mControlQueue.Pop(runnable)) { runnable->Cancel(); runnable->Release(); } } // Unroot the globals data->mScope = nullptr; data->mDebuggerScope = nullptr; return; } } 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); static_cast(runnable)->Run(); runnable->Release(); CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get(); ccjs->PerformDebuggerMicroTaskCheckpoint(); if (debuggerRunnablesPending) { WorkerDebuggerGlobalScope* globalScope = DebuggerGlobalScope(); MOZ_ASSERT(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(mThread, false); normalRunnablesPending = NS_HasPendingEvents(mThread); 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); } } 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 = mMainThreadEventTarget->Length() + mMainThreadDebuggeeEventTarget->Length(); if (queuedEvents > 5000) { mMainThreadEventTarget->AwaitIdle(); } } MOZ_CRASH("Shouldn't get here!"); } void WorkerPrivate::OnProcessNextEvent() { AssertIsOnWorkerThread(); uint32_t recursionDepth = CycleCollectedJSContext::Get()->RecursionDepth(); 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(CycleCollectedJSContext::Get()->RecursionDepth()); } nsIEventTarget* WorkerPrivate::MainThreadEventTarget() { return mMainThreadEventTarget; } nsresult WorkerPrivate::DispatchToMainThread(nsIRunnable* aRunnable, uint32_t aFlags) { nsCOMPtr r = aRunnable; return DispatchToMainThread(r.forget(), aFlags); } nsresult WorkerPrivate::DispatchToMainThread(already_AddRefed aRunnable, uint32_t aFlags) { return mMainThreadEventTarget->Dispatch(std::move(aRunnable), aFlags); } nsresult WorkerPrivate::DispatchDebuggeeToMainThread(already_AddRefed aRunnable, uint32_t aFlags) { return mMainThreadDebuggeeEventTarget->Dispatch(std::move(aRunnable), aFlags); } nsISerialEventTarget* WorkerPrivate::ControlEventTarget() { return mWorkerControlEventTarget; } nsISerialEventTarget* WorkerPrivate::HybridEventTarget() { return mWorkerHybridEventTarget; } bool WorkerPrivate::EnsureClientSource() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); if (data->mClientSource) { return true; } ClientType type; switch(Type()) { case WorkerTypeDedicated: type = ClientType::Worker; break; case WorkerTypeShared: type = ClientType::Sharedworker; break; case WorkerTypeService: type = ClientType::Serviceworker; break; default: MOZ_CRASH("unknown worker type!"); } data->mClientSource = ClientManager::CreateSource(type, mWorkerHybridEventTarget, GetPrincipalInfo()); MOZ_DIAGNOSTIC_ASSERT(data->mClientSource); if (data->mFrozen) { data->mClientSource->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 (Type() != WorkerTypeService && !IsChromeWorker()) { data->mClientSource->WorkerSyncPing(this); } return true; } 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); } } Maybe WorkerPrivate::GetClientInfo() const { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); Maybe clientInfo; if (!data->mClientSource) { MOZ_DIAGNOSTIC_ASSERT(mStatus >= Canceling); return clientInfo; } clientInfo.emplace(data->mClientSource->Info()); return clientInfo; } const ClientState WorkerPrivate::GetClientState() const { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_DIAGNOSTIC_ASSERT(data->mClientSource); ClientState state; data->mClientSource->SnapshotState(&state); return state; } const Maybe WorkerPrivate::GetController() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); { MutexAutoLock lock(mMutex); if (mStatus >= Canceling) { return Maybe(); } } MOZ_DIAGNOSTIC_ASSERT(data->mClientSource); return data->mClientSource->GetController(); } void WorkerPrivate::Control(const ServiceWorkerDescriptor& aServiceWorker) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_DIAGNOSTIC_ASSERT(!IsChromeWorker()); MOZ_DIAGNOSTIC_ASSERT(Type() != WorkerTypeService); { MutexAutoLock lock(mMutex); if (mStatus >= Canceling) { return; } } MOZ_DIAGNOSTIC_ASSERT(data->mClientSource); if (IsBlobURI(mLoadInfo.mBaseURI)) { // Blob URL workers can only become controlled by inheriting from // their parent. Make sure to note this properly. data->mClientSource->InheritController(aServiceWorker); } else { // Otherwise this is a normal interception and we simply record the // controller locally. data->mClientSource->SetController(aServiceWorker); } } void WorkerPrivate::ExecutionReady() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); { MutexAutoLock lock(mMutex); if (mStatus >= Canceling) { return; } } MOZ_DIAGNOSTIC_ASSERT(data->mClientSource); data->mClientSource->WorkerExecutionReady(this); } void WorkerPrivate::InitializeGCTimers() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); // We need a timer 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. If the worker receives more messages then the short // timer is canceled and the periodic timer resumes. data->mGCTimer = NS_NewTimer(); MOZ_ASSERT(data->mGCTimer); data->mPeriodicGCTimerRunning = false; data->mIdleGCTimerRunning = false; } void WorkerPrivate::SetGCTimerMode(GCTimerMode aMode) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_ASSERT(data->mGCTimer); if ((aMode == PeriodicTimer && data->mPeriodicGCTimerRunning) || (aMode == IdleTimer && data->mIdleGCTimerRunning)) { return; } MOZ_ALWAYS_SUCCEEDS(data->mGCTimer->Cancel()); data->mPeriodicGCTimerRunning = false; data->mIdleGCTimerRunning = false; LOG(WorkerLog(), ("Worker %p canceled GC timer because %s\n", this, aMode == PeriodicTimer ? "periodic" : aMode == IdleTimer ? "idle" : "none")); if (aMode == NoTimer) { 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; if (aMode == PeriodicTimer) { delay = PERIODIC_GC_TIMER_DELAY_SEC * 1000; type = nsITimer::TYPE_REPEATING_SLACK; callback = PeriodicGCTimerCallback; name = "dom::PeriodicGCTimerCallback"; } else { delay = IDLE_GC_TIMER_DELAY_SEC * 1000; type = nsITimer::TYPE_ONE_SHOT; callback = IdleGCTimerCallback; name = "dom::IdleGCTimerCallback"; } MOZ_ALWAYS_SUCCEEDS(data->mGCTimer->SetTarget(mWorkerControlEventTarget)); MOZ_ALWAYS_SUCCEEDS( data->mGCTimer->InitWithNamedFuncCallback(callback, this, delay, type, name)); if (aMode == PeriodicTimer) { LOG(WorkerLog(), ("Worker %p scheduled periodic GC timer\n", this)); data->mPeriodicGCTimerRunning = true; } else { LOG(WorkerLog(), ("Worker %p scheduled idle GC timer\n", this)); data->mIdleGCTimerRunning = true; } } void WorkerPrivate::ShutdownGCTimers() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_ASSERT(data->mGCTimer); // Always make sure the timer is canceled. MOZ_ALWAYS_SUCCEEDS(data->mGCTimer->Cancel()); LOG(WorkerLog(), ("Worker %p killed the GC timer\n", this)); data->mGCTimer = nullptr; data->mPeriodicGCTimerRunning = false; data->mIdleGCTimerRunning = false; } bool WorkerPrivate::InterruptCallback(JSContext* aCx) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); 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; if (mayFreeze) { MutexAutoLock lock(mMutex); mayFreeze = mStatus <= Running; } 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) { { // mWorkerThreadAccessible's accessor must be destructed before // the scheduled Runnable gets to run. MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_ASSERT(data->mChildWorkers.IsEmpty()); } MOZ_ASSERT(mSyncLoopStack.IsEmpty()); MOZ_ASSERT(!mPendingEventQueueClearing); ClearMainEventQueue(aRanOrNot); #ifdef DEBUG if (WorkerRan == aRanOrNot) { nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); MOZ_ASSERT(!NS_HasPendingEvents(currentThread)); } #endif if (WorkerPrivate* parent = GetParent()) { RefPtr runnable = new WorkerFinishedRunnable(parent, this); if (!runnable->Dispatch()) { NS_WARNING("Failed to dispatch runnable!"); } } else { RefPtr runnable = new TopLevelWorkerFinishedRunnable(this); if (NS_FAILED(DispatchToMainThread(runnable.forget()))) { NS_WARNING("Failed to dispatch runnable!"); } } } bool WorkerPrivate::CollectRuntimeStats(JS::RuntimeStats* aRtStats, bool aAnonymize) { AssertIsOnWorkerThread(); NS_ASSERTION(aRtStats, "Null RuntimeStats!"); NS_ASSERTION(mJSContext, "This must never be null!"); return JS::CollectRuntimeStats(mJSContext, aRtStats, nullptr, aAnonymize); } void WorkerPrivate::EnableMemoryReporter() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); 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() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); RefPtr 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(); AUTO_PROFILER_THREAD_SLEEP; // Wait for a worker event. mCondVar.Wait(); } WorkerPrivate::ProcessAllControlRunnablesResult WorkerPrivate::ProcessAllControlRunnablesLocked() { AssertIsOnWorkerThread(); mMutex.AssertCurrentThreadOwns(); auto result = ProcessAllControlRunnablesResult::Nothing; for (;;) { WorkerControlRunnable* event; if (!mControlQueue.Pop(event)) { break; } MutexAutoUnlock unlock(mMutex); MOZ_ASSERT(event); if (NS_FAILED(static_cast(event)->Run())) { result = ProcessAllControlRunnablesResult::Abort; } if (result == ProcessAllControlRunnablesResult::Nothing) { // We ran at least one thing. result = ProcessAllControlRunnablesResult::MayContinue; } event->Release(); } return result; } void WorkerPrivate::ClearMainEventQueue(WorkerRanOrNot aRanOrNot) { AssertIsOnWorkerThread(); MOZ_ASSERT(mSyncLoopStack.IsEmpty()); MOZ_ASSERT(!mCancelAllPendingRunnables); mCancelAllPendingRunnables = true; if (WorkerNeverRan == aRanOrNot) { for (uint32_t count = mPreStartRunnables.Length(), index = 0; index < count; index++) { RefPtr runnable = mPreStartRunnables[index].forget(); static_cast(runnable.get())->Run(); } } else { nsIThread* currentThread = NS_GetCurrentThread(); MOZ_ASSERT(currentThread); NS_ProcessPendingEvents(currentThread); } MOZ_ASSERT(mCancelAllPendingRunnables); mCancelAllPendingRunnables = false; } 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() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); NS_ASSERTION(!data->mFrozen, "Already frozen!"); if (data->mClientSource) { data->mClientSource->Freeze(); } data->mFrozen = true; for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { data->mChildWorkers[index]->Freeze(nullptr); } return true; } bool WorkerPrivate::ThawInternal() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); 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; if (data->mClientSource) { data->mClientSource->Thaw(); } return true; } void WorkerPrivate::PropagateFirstPartyStorageAccessGrantedInternal() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); mLoadInfo.mFirstPartyStorageAccessGranted = true; for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { data->mChildWorkers[index]->PropagateFirstPartyStorageAccessGranted(); } } void WorkerPrivate::TraverseTimeouts(nsCycleCollectionTraversalCallback& cb) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); for (uint32_t i = 0; i < data->mTimeouts.Length(); ++i) { TimeoutInfo* tmp = data->mTimeouts[i]; NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHandler) } } void WorkerPrivate::UnlinkTimeouts() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); data->mTimeouts.Clear(); } bool WorkerPrivate::ModifyBusyCountFromWorker(bool aIncrease) { AssertIsOnWorkerThread(); { MutexAutoLock lock(mMutex); // If we're in shutdown then the busy count is no longer being considered so // just return now. if (mStatus >= Killing) { return true; } } RefPtr runnable = new ModifyBusyCountRunnable(this, aIncrease); return runnable->Dispatch(); } bool WorkerPrivate::AddChildWorker(WorkerPrivate* aChildWorker) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); #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); return data->mChildWorkers.Length() == 1 ? ModifyBusyCountFromWorker(true) : true; } void WorkerPrivate::RemoveChildWorker(WorkerPrivate* aChildWorker) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); NS_ASSERTION(data->mChildWorkers.Contains(aChildWorker), "Didn't know about this one!"); data->mChildWorkers.RemoveElement(aChildWorker); if (data->mChildWorkers.IsEmpty() && !ModifyBusyCountFromWorker(false)) { NS_WARNING("Failed to modify busy count!"); } } bool WorkerPrivate::AddHolder(WorkerHolder* aHolder, WorkerStatus aFailStatus) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); { MutexAutoLock lock(mMutex); if (mStatus >= aFailStatus) { return false; } } MOZ_ASSERT(!data->mHolders.Contains(aHolder), "Already know about this one!"); if (aHolder->GetBehavior() == WorkerHolder::PreventIdleShutdownStart) { if (!data->mNumHoldersPreventingShutdownStart && !ModifyBusyCountFromWorker(true)) { return false; } data->mNumHoldersPreventingShutdownStart += 1; } data->mHolders.AppendElement(aHolder); return true; } void WorkerPrivate::RemoveHolder(WorkerHolder* aHolder) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_ASSERT(data->mHolders.Contains(aHolder), "Didn't know about this one!"); data->mHolders.RemoveElement(aHolder); if (aHolder->GetBehavior() == WorkerHolder::PreventIdleShutdownStart) { data->mNumHoldersPreventingShutdownStart -= 1; if (!data->mNumHoldersPreventingShutdownStart && !ModifyBusyCountFromWorker(false)) { NS_WARNING("Failed to modify busy count!"); } } } void WorkerPrivate::NotifyHolders(WorkerStatus aStatus) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); NS_ASSERTION(aStatus > Closing, "Bad status!"); nsTObserverArray::ForwardIterator iter(data->mHolders); while (iter.HasMore()) { WorkerHolder* holder = iter.GetNext(); if (!holder->Notify(aStatus)) { NS_WARNING("Failed to notify holder!"); } } AutoTArray 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!"); } } } void WorkerPrivate::CancelAllTimeouts() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); 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(); ModifyBusyCountFromWorker(false); } // 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 WorkerPrivate::CreateNewSyncLoop(WorkerStatus aFailStatus) { AssertIsOnWorkerThread(); MOZ_ASSERT(aFailStatus >= Canceling, "Sync loops can be created when the worker is in Running/Closing state!"); { MutexAutoLock lock(mMutex); if (mStatus >= aFailStatus) { return nullptr; } } auto queue = static_cast*>(mThread->EventQueue()); nsCOMPtr realEventTarget = queue->PushEventQueue(); MOZ_ASSERT(realEventTarget); RefPtr workerEventTarget = new EventTarget(this, realEventTarget); { // 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(); } bool WorkerPrivate::RunCurrentSyncLoop() { AssertIsOnWorkerThread(); JSContext* cx = GetJSContext(); MOZ_ASSERT(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]; 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(mThread)) { SetGCTimerMode(IdleTimer); } // Wait for something to do. { MutexAutoLock lock(mMutex); for (;;) { while (mControlQueue.IsEmpty() && !normalRunnablesPending && !(normalRunnablesPending = NS_HasPendingEvents(mThread))) { WaitForWorkerEvents(); } auto result = ProcessAllControlRunnablesLocked(); if (result != ProcessAllControlRunnablesResult::Nothing) { // XXXkhuey how should we handle Abort here? See Bug 1003730. // The state of the world may have changed. Recheck it. normalRunnablesPending = NS_HasPendingEvents(mThread); // 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(mThread, false)); // Now *might* be a good time to GC. Let the JS engine make the decision. if (JS::CurrentGlobalOrNull(cx)) { JS_MaybeGC(cx); } } } // Make sure that the stack didn't change underneath us. MOZ_ASSERT(mSyncLoopStack[currentLoopIndex] == loopInfo); return DestroySyncLoop(currentLoopIndex); } bool WorkerPrivate::DestroySyncLoop(uint32_t aLoopIndex) { MOZ_ASSERT(!mSyncLoopStack.IsEmpty()); MOZ_ASSERT(mSyncLoopStack.Length() - 1 == aLoopIndex); // We're about to delete the loop, stash its event target and result. SyncLoopInfo* loopInfo = mSyncLoopStack[aLoopIndex]; nsIEventTarget* nestedEventTarget = loopInfo->mEventTarget->GetWeakNestedEventTarget(); MOZ_ASSERT(nestedEventTarget); bool result = loopInfo->mResult; { // 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); } auto queue = static_cast*>(mThread->EventQueue()); queue->PopEventQueue(nestedEventTarget); if (mSyncLoopStack.IsEmpty() && mPendingEventQueueClearing) { mPendingEventQueueClearing = false; ClearMainEventQueue(WorkerRan); } return result; } void WorkerPrivate::StopSyncLoop(nsIEventTarget* aSyncLoopTarget, bool aResult) { AssertIsOnWorkerThread(); AssertValidSyncLoop(aSyncLoopTarget); MOZ_ASSERT(!mSyncLoopStack.IsEmpty()); for (uint32_t index = mSyncLoopStack.Length(); index > 0; index--) { nsAutoPtr& 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; } MOZ_ASSERT(!SameCOMIdentity(loopInfo->mEventTarget, aSyncLoopTarget)); } MOZ_CRASH("Unknown sync loop!"); } #ifdef DEBUG void WorkerPrivate::AssertValidSyncLoop(nsIEventTarget* aSyncLoopTarget) { MOZ_ASSERT(aSyncLoopTarget); EventTarget* workerTarget; nsresult rv = aSyncLoopTarget->QueryInterface(kDEBUGWorkerEventTargetIID, reinterpret_cast(&workerTarget)); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(workerTarget); bool valid = false; { MutexAutoLock lock(mMutex); for (uint32_t index = 0; index < mSyncLoopStack.Length(); index++) { nsAutoPtr& 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 aMessage, const Sequence& aTransferable, ErrorResult& aRv) { AssertIsOnWorkerThread(); JS::Rooted transferable(aCx, JS::UndefinedValue()); aRv = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransferable, &transferable); if (NS_WARN_IF(aRv.Failed())) { return; } RefPtr runnable = new MessageEventRunnable(this, WorkerRunnable::ParentThreadUnchangedBusyCount); UniquePtr start; UniquePtr end; RefPtr timelines = TimelineConsumers::Get(); bool isTimelineRecording = timelines && !timelines->IsEmpty(); if (isTimelineRecording) { start = MakeUnique(NS_IsMainThread() ? ProfileTimelineWorkerOperationType::SerializeDataOnMainThread : ProfileTimelineWorkerOperationType::SerializeDataOffMainThread, MarkerTracingType::START); } runnable->Write(aCx, aMessage, transferable, JS::CloneDataPolicy(), aRv); if (isTimelineRecording) { end = MakeUnique(NS_IsMainThread() ? ProfileTimelineWorkerOperationType::SerializeDataOnMainThread : ProfileTimelineWorkerOperationType::SerializeDataOffMainThread, MarkerTracingType::END); timelines->AddMarkerForAllObservedDocShells(start); timelines->AddMarkerForAllObservedDocShells(end); } if (NS_WARN_IF(aRv.Failed())) { return; } if (!runnable->Dispatch()) { aRv = NS_ERROR_FAILURE; } } void WorkerPrivate::EnterDebuggerEventLoop() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); JSContext* cx = GetJSContext(); MOZ_ASSERT(cx); 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::queue>& 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(runnable)->Run(); runnable->Release(); ccjscx->PerformDebuggerMicroTaskCheckpoint(); // Now *might* be a good time to GC. Let the JS engine make the decision. if (JS::CurrentGlobalOrNull(cx)) { JS_MaybeGC(cx); } } } } void WorkerPrivate::LeaveDebuggerEventLoop() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); // 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 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) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); NS_ASSERTION(aStatus > Running && aStatus < Dead, "Bad status!"); RefPtr eventTarget; // Save the old status and set the new status. WorkerStatus previousStatus; { MutexAutoLock lock(mMutex); if (mStatus >= aStatus) { return true; } if (aStatus >= Canceling) { MutexAutoUnlock unlock(mMutex); data->mClientSource.reset(); if (data->mScope) { data->mScope->NoteTerminating(); } } // Make sure the hybrid event target stops dispatching runnables // once we reaching the killing state. if (aStatus == Killing) { // To avoid deadlock we always acquire the event target mutex before the // worker private mutex. (We do it in this order because this is what // workers best for event dispatching.) To enforce that order here we // need to unlock the worker private mutex before we lock the event target // mutex in ForgetWorkerPrivate. { MutexAutoUnlock unlock(mMutex); mWorkerHybridEventTarget->ForgetWorkerPrivate(this); } // Check the status code again in case another NotifyInternal came in // while we were unlocked above. if (mStatus >= aStatus) { return true; } } previousStatus = mStatus; mStatus = aStatus; // Mark parent status as closing immediately to avoid new events being // dispatched after we clear the queue below. if (aStatus == Closing) { Close(); } } MOZ_ASSERT(previousStatus != Pending); if (aStatus >= Closing) { CancelAllTimeouts(); } // Let all our holders know the new status. if (aStatus > Closing) { NotifyHolders(aStatus); } // If this is the first time our status has changed then we need to clear the // main event queue. if (previousStatus == Running) { // NB: If we're in a sync loop, we can't clear the queue immediately, // because this is the wrong queue. So we have to defer it until later. if (!mSyncLoopStack.IsEmpty()) { mPendingEventQueueClearing = true; } else { ClearMainEventQueue(WorkerRan); } } // If the worker script never ran, or failed to compile, we don't need to do // anything else. if (!GlobalScope()) { 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()) { // 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. RefPtr r = new CancelingRunnable(); 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. RefPtr rr = new CancelingWithTimeoutOnParentRunnable(this); rr->Dispatch(); } return true; } MOZ_ASSERT(aStatus == Canceling || aStatus == Killing); // Always abort the script. return false; } void WorkerPrivate::ReportError(JSContext* aCx, JS::ConstUTF8CharsZ aToStringResult, JSErrorReport* aReport) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); if (!MayContinueRunning() || data->mErrorHandlerRecursionCount == 2) { return; } NS_ASSERTION(data->mErrorHandlerRecursionCount == 0 || data->mErrorHandlerRecursionCount == 1, "Bad recursion logic!"); JS::Rooted exn(aCx); if (!JS_GetPendingException(aCx, &exn)) { // Probably shouldn't actually happen? But let's go ahead and just use null // for lack of anything better. exn.setNull(); } JS_ClearPendingException(aCx); WorkerErrorReport report; if (aReport) { report.AssignErrorReport(aReport); } else { report.mFlags = nsIScriptError::errorFlag | nsIScriptError::exceptionFlag; } 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. uint32_t index = std::min(uint32_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, report, 0, exn); data->mErrorHandlerRecursionCount--; } // static void WorkerPrivate::ReportErrorToConsole(const char* aMessage) { nsTArray emptyParams; WorkerPrivate::ReportErrorToConsole(aMessage, emptyParams); } // static void WorkerPrivate::ReportErrorToConsole(const char* aMessage, const nsTArray& aParams) { WorkerPrivate* wp = nullptr; if (!NS_IsMainThread()) { wp = GetCurrentThreadWorkerPrivate(); } ReportErrorToConsoleRunnable::Report(wp, aMessage, aParams); } int32_t WorkerPrivate::SetTimeout(JSContext* aCx, nsIScriptTimeoutHandler* aHandler, int32_t aTimeout, bool aIsInterval, ErrorResult& aRv) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_ASSERT(aHandler); const int32_t timerId = data->mNextTimeoutId++; 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; } nsAutoPtr newInfo(new TimeoutInfo()); newInfo->mIsInterval = aIsInterval; newInfo->mId = timerId; if (MOZ_UNLIKELY(timerId == INT32_MAX)) { NS_WARNING("Timeout ids overflowed!"); 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->mTargetTime = TimeStamp::Now() + newInfo->mInterval; nsAutoPtr* insertedInfo = data->mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(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(); if (!data->mTimer) { aRv.Throw(NS_ERROR_UNEXPECTED); return 0; } data->mTimerRunnable = new TimerRunnable(this); } if (!data->mTimerRunning) { if (!ModifyBusyCountFromWorker(true)) { aRv.Throw(NS_ERROR_FAILURE); return 0; } data->mTimerRunning = true; } if (!RescheduleTimeoutTimer(aCx)) { aRv.Throw(NS_ERROR_FAILURE); return 0; } } return timerId; } void WorkerPrivate::ClearTimeout(int32_t aId) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); if (!data->mTimeouts.IsEmpty()) { NS_ASSERTION(data->mTimerRunning, "Huh?!"); for (uint32_t index = 0; index < data->mTimeouts.Length(); index++) { nsAutoPtr& info = data->mTimeouts[index]; if (info->mId == aId) { info->mCanceled = true; break; } } } } bool WorkerPrivate::RunExpiredTimeouts(JSContext* aCx) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); // 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; AutoPtrComparator comparator = GetAutoPtrComparator(data->mTimeouts); JS::Rooted 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())); } AutoTArray expiredTimeouts; for (uint32_t index = 0; index < data->mTimeouts.Length(); index++) { nsAutoPtr& info = data->mTimeouts[index]; if (info->mTargetTime > now) { break; } expiredTimeouts.AppendElement(info); } // Guard against recursion. data->mRunningExpiredTimeouts = true; // Run expired timeouts. for (uint32_t index = 0; index < expiredTimeouts.Length(); index++) { TimeoutInfo*& info = expiredTimeouts[index]; if (info->mCanceled) { continue; } 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. const char *reason; if (info->mIsInterval) { reason = "setInterval handler"; } else { reason = "setTimeout handler"; } RefPtr callback = info->mHandler->GetCallback(); if (!callback) { nsAutoMicroTask mt; AutoEntryScript aes(global, reason, false); // Evaluate the timeout expression. const nsAString& script = info->mHandler->GetHandlerText(); const char* filename = nullptr; uint32_t lineNo = 0, dummyColumn = 0; info->mHandler->GetLocation(&filename, &lineNo, &dummyColumn); JS::CompileOptions options(aes.cx()); options.setFileAndLine(filename, lineNo).setNoScriptRval(true); JS::Rooted unused(aes.cx()); JS::SourceText srcBuf; if (!srcBuf.init(aes.cx(), script.BeginReading(), script.Length(), JS::SourceOwnership::Borrowed) || !JS::Evaluate(aes.cx(), options, srcBuf, &unused)) { if (!JS_IsExceptionPending(aCx)) { retval = false; break; } } } else { ErrorResult rv; JS::Rooted ignoredVal(aCx); callback->Call(GlobalScope(), info->mHandler->GetArgs(), &ignoredVal, rv, reason); if (rv.IsUncatchableException()) { rv.SuppressException(); retval = false; break; } rv.SuppressException(); } 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(); ) { nsAutoPtr& info = data->mTimeouts[index]; if ((expiredTimeoutIndex < expiredTimeoutLength && info == expiredTimeouts[expiredTimeoutIndex] && ++expiredTimeoutIndex) || info->mCanceled) { if (info->mIsInterval && !info->mCanceled) { // Reschedule intervals. info->mTargetTime = info->mTargetTime + info->mInterval; // 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()) { if (!ModifyBusyCountFromWorker(false)) { retval = false; } data->mTimerRunning = false; } else if (retval && !RescheduleTimeoutTimer(aCx)) { retval = false; } return retval; } bool WorkerPrivate::RescheduleTimeoutTimer(JSContext* aCx) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); 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 ? 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(); auto errorCleanup = MakeScopeExit([&] { mCancelingTimer = nullptr; }); MOZ_ASSERT(!mCancelingTimer); 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 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) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); JS::ContextOptionsRef(aCx) = aContextOptions; for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { data->mChildWorkers[index]->UpdateContextOptions(aContextOptions); } } void WorkerPrivate::UpdateLanguagesInternal(const nsTArray& aLanguages) { WorkerGlobalScope* globalScope = GlobalScope(); if (globalScope) { RefPtr nav = globalScope->GetExistingNavigator(); if (nav) { nav->SetLanguages(aLanguages); } } MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { data->mChildWorkers[index]->UpdateLanguages(aLanguages); } } void WorkerPrivate::UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, JSGCParamKey aKey, uint32_t aValue) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); // XXX aValue might be 0 here (telling us to unset a previous value for child // workers). Calling JS_SetGCParameter with a value of 0 isn't actually // supported though. We really need some way to revert to a default value // here. if (aValue) { JS_SetGCParameter(aCx, aKey, aValue); } 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) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); JS_SetGCZeal(aCx, aGCZeal, aFrequency); for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { data->mChildWorkers[index]->UpdateGCZeal(aGCZeal, aFrequency); } } #endif void WorkerPrivate::GarbageCollectInternal(JSContext* aCx, bool aShrinking, bool aCollectChildren) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); if (!GlobalScope()) { // We haven't compiled anything yet. Just bail out. return; } if (aShrinking || aCollectChildren) { JS::PrepareForFullGC(aCx); if (aShrinking) { JS::NonIncrementalGC(aCx, GC_SHRINK, JS::gcreason::DOM_WORKER); if (!aCollectChildren) { LOG(WorkerLog(), ("Worker %p collected idle garbage\n", this)); } } else { JS::NonIncrementalGC(aCx, GC_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) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); nsCycleCollector_collect(nullptr); if (aCollectChildren) { for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { data->mChildWorkers[index]->CycleCollect(/* aDummy = */ false); } } } void WorkerPrivate::MemoryPressureInternal() { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); if (data->mScope) { RefPtr console = data->mScope->GetConsoleIfExists(); if (console) { console->ClearStorage(); } RefPtr performance = data->mScope->GetPerformanceIfExists(); if (performance) { performance->MemoryPressure(); } } if (data->mDebuggerScope) { RefPtr console = data->mDebuggerScope->GetConsoleIfExists(); if (console) { console->ClearStorage(); } } for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { data->mChildWorkers[index]->MemoryPressure(false); } } 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) { 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() { RefPtr doomedThread; // Release the mutex before doomedThread. MutexAutoLock lock(mMutex); MOZ_ASSERT(mThread); mThread->SetWorker(WorkerThreadFriendKey{}, nullptr); mThread.swap(doomedThread); } void WorkerPrivate::BeginCTypesCall() { AssertIsOnWorkerThread(); // Don't try to GC while we're blocked in a ctypes call. SetGCTimerMode(NoTimer); } void WorkerPrivate::EndCTypesCall() { AssertIsOnWorkerThread(); // Make sure the periodic timer is running before we start running JS again. SetGCTimerMode(PeriodicTimer); } bool WorkerPrivate::ConnectMessagePort(JSContext* aCx, const MessagePortIdentifier& aIdentifier) { AssertIsOnWorkerThread(); WorkerGlobalScope* globalScope = GlobalScope(); JS::Rooted jsGlobal(aCx, globalScope->GetWrapper()); MOZ_ASSERT(jsGlobal); // This MessagePortIdentifier is used to create a new port, still connected // with the other one, but in the worker thread. ErrorResult rv; RefPtr 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 init(aCx); init.mBubbles = false; init.mCancelable = false; init.mSource.SetValue().SetAsMessagePort() = port; if (!init.mPorts.AppendElement(port.forget(), fallible)) { return false; } RefPtr event = MessageEvent::Constructor(globalObject, NS_LITERAL_STRING("connect"), init, rv); event->SetTrusted(true); globalScope->DispatchEvent(*event); return true; } WorkerGlobalScope* WorkerPrivate::GetOrCreateGlobalScope(JSContext* aCx) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); if (!data->mScope) { RefPtr globalScope; if (IsSharedWorker()) { globalScope = new SharedWorkerGlobalScope(this, WorkerName()); } else if (IsServiceWorker()) { globalScope = new ServiceWorkerGlobalScope(this, GetServiceWorkerRegistrationDescriptor()); } else { globalScope = new DedicatedWorkerGlobalScope(this, WorkerName()); } JS::Rooted global(aCx); NS_ENSURE_TRUE(globalScope->WrapGlobalObject(aCx, &global), nullptr); JSAutoRealm ar(aCx, global); // RegisterBindings() can spin a nested event loop so we have to set mScope // before calling it, and we have to make sure to unset mScope if it fails. data->mScope = std::move(globalScope); if (!RegisterBindings(aCx, global)) { data->mScope = nullptr; return nullptr; } JS_FireOnNewGlobalObject(aCx, global); } return data->mScope; } WorkerDebuggerGlobalScope* WorkerPrivate::CreateDebuggerGlobalScope(JSContext* aCx) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); MOZ_ASSERT(!data->mDebuggerScope); RefPtr globalScope = new WorkerDebuggerGlobalScope(this); JS::Rooted global(aCx); NS_ENSURE_TRUE(globalScope->WrapGlobalObject(aCx, &global), nullptr); JSAutoRealm ar(aCx, global); // RegisterDebuggerBindings() can spin a nested event loop so we have to set // mDebuggerScope before calling it, and we have to make sure to unset // mDebuggerScope if it fails. data->mDebuggerScope = std::move(globalScope); 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) { MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data); nsTObserverArray::ForwardIterator iter(data->mHolders); while (iter.HasMore()) { WorkerHolder* holder = iter.GetNext(); aString.Append("|"); aString.Append(holder->Name()); } } void WorkerPrivate::EnsurePerformanceCounter() { AssertIsOnWorkerThread(); MOZ_ASSERT(mozilla::StaticPrefs::dom_performance_enable_scheduler_timing()); if (!mPerformanceCounter) { nsPrintfCString workerName("Worker:%s", NS_ConvertUTF16toUTF8(mWorkerName).get()); mPerformanceCounter = new PerformanceCounter(workerName); } } PerformanceCounter* WorkerPrivate::GetPerformanceCounter() { return mPerformanceCounter; } PerformanceStorage* WorkerPrivate::GetPerformanceStorage() { AssertIsOnMainThread(); MOZ_ASSERT(mPerformanceStorage); return mPerformanceStorage; } void WorkerPrivate::SetRemoteWorkerController(RemoteWorkerChild* aController) { AssertIsOnMainThread(); MOZ_ASSERT(aController); MOZ_ASSERT(!mRemoteWorkerController); mRemoteWorkerController = aController; } RemoteWorkerChild* WorkerPrivate::GetRemoteWorkerController() { AssertIsOnMainThread(); MOZ_ASSERT(mRemoteWorkerController); return mRemoteWorkerController; } 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 event(aRunnable); return Dispatch(event.forget(), aFlags); } NS_IMETHODIMP WorkerPrivate::EventTarget::Dispatch(already_AddRefed aRunnable, uint32_t aFlags) { // May be called on any thread! nsCOMPtr event(aRunnable); // Workers only support asynchronous dispatch for now. if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { return NS_ERROR_UNEXPECTED; } RefPtr workerRunnable; MutexAutoLock lock(mMutex); if (!mWorkerPrivate) { NS_WARNING("A runnable was posted to a worker that is already shutting " "down!"); return NS_ERROR_UNEXPECTED; } 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, uint32_t) { 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 (!mWorkerPrivate) { NS_WARNING("A worker's event target was used after the worker has !"); return NS_ERROR_UNEXPECTED; } *aIsOnCurrentThread = mWorkerPrivate->IsOnCurrentThread(); return NS_OK; } NS_IMETHODIMP_(bool) WorkerPrivate::EventTarget::IsOnCurrentThreadInfallible() { // May be called on any thread! MutexAutoLock lock(mMutex); if (!mWorkerPrivate) { NS_WARNING("A worker's event target was used after the worker has !"); return false; } return mWorkerPrivate->IsOnCurrentThread(); } } // dom namespace } // mozilla namespace