/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/Promise.h" #include "js/Debug.h" #include "mozilla/Atomics.h" #include "mozilla/CycleCollectedJSContext.h" #include "mozilla/OwningNonNull.h" #include "mozilla/Preferences.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/DOMException.h" #include "mozilla/dom/DOMExceptionBinding.h" #include "mozilla/dom/MediaStreamError.h" #include "mozilla/dom/PromiseBinding.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerRunnable.h" #include "mozilla/dom/WorkerRef.h" #include "jsfriendapi.h" #include "js/StructuredClone.h" #include "nsContentUtils.h" #include "nsGlobalWindow.h" #include "nsIScriptObjectPrincipal.h" #include "nsJSEnvironment.h" #include "nsJSPrincipals.h" #include "nsJSUtils.h" #include "nsPIDOMWindow.h" #include "PromiseDebugging.h" #include "PromiseNativeHandler.h" #include "PromiseWorkerProxy.h" #include "WrapperFactory.h" #include "xpcpublic.h" namespace mozilla { namespace dom { namespace { // Generator used by Promise::GetID. Atomic gIDGenerator(0); } // namespace // Promise NS_IMPL_CYCLE_COLLECTION_CLASS(Promise) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Promise) NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) tmp->mPromiseObj = nullptr; NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Promise) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Promise) NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPromiseObj); NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(Promise) NS_IMPL_CYCLE_COLLECTING_RELEASE(Promise) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Promise) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_ENTRY(Promise) NS_INTERFACE_MAP_END Promise::Promise(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) , mPromiseObj(nullptr) { MOZ_ASSERT(mGlobal); mozilla::HoldJSObjects(this); } Promise::~Promise() { mozilla::DropJSObjects(this); } // static already_AddRefed Promise::Create(nsIGlobalObject* aGlobal, ErrorResult& aRv) { if (!aGlobal) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } RefPtr p = new Promise(aGlobal); p->CreateWrapper(nullptr, aRv); if (aRv.Failed()) { return nullptr; } return p.forget(); } // static already_AddRefed Promise::Resolve(nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { JSAutoCompartment ac(aCx, aGlobal->GetGlobalJSObject()); JS::Rooted p(aCx, JS::CallOriginalPromiseResolve(aCx, aValue)); if (!p) { aRv.NoteJSContextException(aCx); return nullptr; } return CreateFromExisting(aGlobal, p); } // static already_AddRefed Promise::Reject(nsIGlobalObject* aGlobal, JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { JSAutoCompartment ac(aCx, aGlobal->GetGlobalJSObject()); JS::Rooted p(aCx, JS::CallOriginalPromiseReject(aCx, aValue)); if (!p) { aRv.NoteJSContextException(aCx); return nullptr; } return CreateFromExisting(aGlobal, p); } // static already_AddRefed Promise::All(JSContext* aCx, const nsTArray>& aPromiseList, ErrorResult& aRv) { JS::Rooted globalObj(aCx, JS::CurrentGlobalOrNull(aCx)); if (!globalObj) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } nsCOMPtr global = xpc::NativeGlobal(globalObj); if (!global) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } JS::AutoObjectVector promises(aCx); if (!promises.reserve(aPromiseList.Length())) { aRv.NoteJSContextException(aCx); return nullptr; } for (auto& promise : aPromiseList) { JS::Rooted promiseObj(aCx, promise->PromiseObj()); // Just in case, make sure these are all in the context compartment. if (!JS_WrapObject(aCx, &promiseObj)) { aRv.NoteJSContextException(aCx); return nullptr; } promises.infallibleAppend(promiseObj); } JS::Rooted result(aCx, JS::GetWaitForAllPromise(aCx, promises)); if (!result) { aRv.NoteJSContextException(aCx); return nullptr; } return CreateFromExisting(global, result); } void Promise::Then(JSContext* aCx, // aCalleeGlobal may not be in the compartment of aCx, when called over // Xrays. JS::Handle aCalleeGlobal, AnyCallback* aResolveCallback, AnyCallback* aRejectCallback, JS::MutableHandle aRetval, ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(Promise); // Let's hope this does the right thing with Xrays... Ensure everything is // just in the caller compartment; that ought to do the trick. In theory we // should consider aCalleeGlobal, but in practice our only caller is // DOMRequest::Then, which is not working with a Promise subclass, so things // should be OK. JS::Rooted promise(aCx, PromiseObj()); if (!JS_WrapObject(aCx, &promise)) { aRv.NoteJSContextException(aCx); return; } JS::Rooted resolveCallback(aCx); if (aResolveCallback) { resolveCallback = aResolveCallback->CallbackOrNull(); if (!JS_WrapObject(aCx, &resolveCallback)) { aRv.NoteJSContextException(aCx); return; } } JS::Rooted rejectCallback(aCx); if (aRejectCallback) { rejectCallback = aRejectCallback->CallbackOrNull(); if (!JS_WrapObject(aCx, &rejectCallback)) { aRv.NoteJSContextException(aCx); return; } } JS::Rooted retval(aCx); retval = JS::CallOriginalPromiseThen(aCx, promise, resolveCallback, rejectCallback); if (!retval) { aRv.NoteJSContextException(aCx); return; } aRetval.setObject(*retval); } void Promise::CreateWrapper(JS::Handle aDesiredProto, ErrorResult& aRv) { AutoJSAPI jsapi; if (!jsapi.Init(mGlobal)) { aRv.Throw(NS_ERROR_UNEXPECTED); return; } JSContext* cx = jsapi.cx(); mPromiseObj = JS::NewPromiseObject(cx, nullptr, aDesiredProto); if (!mPromiseObj) { JS_ClearPendingException(cx); aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return; } } void Promise::MaybeResolve(JSContext* aCx, JS::Handle aValue) { NS_ASSERT_OWNINGTHREAD(Promise); JS::Rooted p(aCx, PromiseObj()); if (!JS::ResolvePromise(aCx, p, aValue)) { // Now what? There's nothing sane to do here. JS_ClearPendingException(aCx); } } void Promise::MaybeReject(JSContext* aCx, JS::Handle aValue) { NS_ASSERT_OWNINGTHREAD(Promise); JS::Rooted p(aCx, PromiseObj()); if (!JS::RejectPromise(aCx, p, aValue)) { // Now what? There's nothing sane to do here. JS_ClearPendingException(aCx); } } #define SLOT_NATIVEHANDLER 0 #define SLOT_NATIVEHANDLER_TASK 1 enum class NativeHandlerTask : int32_t { Resolve, Reject }; static bool NativeHandlerCallback(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { JS::CallArgs args = CallArgsFromVp(aArgc, aVp); JS::Value v = js::GetFunctionNativeReserved(&args.callee(), SLOT_NATIVEHANDLER); MOZ_ASSERT(v.isObject()); JS::Rooted obj(aCx, &v.toObject()); PromiseNativeHandler* handler = nullptr; if (NS_FAILED(UNWRAP_OBJECT(PromiseNativeHandler, &obj, handler))) { return Throw(aCx, NS_ERROR_UNEXPECTED); } v = js::GetFunctionNativeReserved(&args.callee(), SLOT_NATIVEHANDLER_TASK); NativeHandlerTask task = static_cast(v.toInt32()); if (task == NativeHandlerTask::Resolve) { handler->ResolvedCallback(aCx, args.get(0)); } else { MOZ_ASSERT(task == NativeHandlerTask::Reject); handler->RejectedCallback(aCx, args.get(0)); } return true; } static JSObject* CreateNativeHandlerFunction(JSContext* aCx, JS::Handle aHolder, NativeHandlerTask aTask) { JSFunction* func = js::NewFunctionWithReserved(aCx, NativeHandlerCallback, /* nargs = */ 1, /* flags = */ 0, nullptr); if (!func) { return nullptr; } JS::Rooted obj(aCx, JS_GetFunctionObject(func)); JS::ExposeObjectToActiveJS(aHolder); js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER, JS::ObjectValue(*aHolder)); js::SetFunctionNativeReserved(obj, SLOT_NATIVEHANDLER_TASK, JS::Int32Value(static_cast(aTask))); return obj; } namespace { class PromiseNativeHandlerShim final : public PromiseNativeHandler { RefPtr mInner; ~PromiseNativeHandlerShim() { } public: explicit PromiseNativeHandlerShim(PromiseNativeHandler* aInner) : mInner(aInner) { MOZ_ASSERT(mInner); } void ResolvedCallback(JSContext* aCx, JS::Handle aValue) override { mInner->ResolvedCallback(aCx, aValue); mInner = nullptr; } void RejectedCallback(JSContext* aCx, JS::Handle aValue) override { mInner->RejectedCallback(aCx, aValue); mInner = nullptr; } bool WrapObject(JSContext* aCx, JS::Handle aGivenProto, JS::MutableHandle aWrapper) { return PromiseNativeHandlerBinding::Wrap(aCx, this, aGivenProto, aWrapper); } NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(PromiseNativeHandlerShim) }; NS_IMPL_CYCLE_COLLECTION(PromiseNativeHandlerShim, mInner) NS_IMPL_CYCLE_COLLECTING_ADDREF(PromiseNativeHandlerShim) NS_IMPL_CYCLE_COLLECTING_RELEASE(PromiseNativeHandlerShim) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PromiseNativeHandlerShim) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END } // anonymous namespace void Promise::AppendNativeHandler(PromiseNativeHandler* aRunnable) { NS_ASSERT_OWNINGTHREAD(Promise); AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(mGlobal))) { // Our API doesn't allow us to return a useful error. Not like this should // happen anyway. return; } // The self-hosted promise js may keep the object we pass to it alive // for quite a while depending on when GC runs. Therefore, pass a shim // object instead. The shim will free its inner PromiseNativeHandler // after the promise has settled just like our previous c++ promises did. RefPtr shim = new PromiseNativeHandlerShim(aRunnable); JSContext* cx = jsapi.cx(); JS::Rooted handlerWrapper(cx); // Note: PromiseNativeHandler is NOT wrappercached. So we can't use // ToJSValue here, because it will try to do XPConnect wrapping on it, sadly. if (NS_WARN_IF(!shim->WrapObject(cx, nullptr, &handlerWrapper))) { // Again, no way to report errors. jsapi.ClearException(); return; } JS::Rooted resolveFunc(cx); resolveFunc = CreateNativeHandlerFunction(cx, handlerWrapper, NativeHandlerTask::Resolve); if (NS_WARN_IF(!resolveFunc)) { jsapi.ClearException(); return; } JS::Rooted rejectFunc(cx); rejectFunc = CreateNativeHandlerFunction(cx, handlerWrapper, NativeHandlerTask::Reject); if (NS_WARN_IF(!rejectFunc)) { jsapi.ClearException(); return; } JS::Rooted promiseObj(cx, PromiseObj()); if (NS_WARN_IF(!JS::AddPromiseReactions(cx, promiseObj, resolveFunc, rejectFunc))) { jsapi.ClearException(); return; } } void Promise::HandleException(JSContext* aCx) { JS::Rooted exn(aCx); if (JS_GetPendingException(aCx, &exn)) { JS_ClearPendingException(aCx); // This is only called from MaybeSomething, so it's OK to MaybeReject here. MaybeReject(aCx, exn); } } // static already_AddRefed Promise::CreateFromExisting(nsIGlobalObject* aGlobal, JS::Handle aPromiseObj) { MOZ_ASSERT(js::GetObjectCompartment(aGlobal->GetGlobalJSObject()) == js::GetObjectCompartment(aPromiseObj)); RefPtr p = new Promise(aGlobal); p->mPromiseObj = aPromiseObj; return p.forget(); } void Promise::MaybeResolveWithUndefined() { NS_ASSERT_OWNINGTHREAD(Promise); MaybeResolve(JS::UndefinedHandleValue); } void Promise::MaybeReject(const RefPtr& aArg) { NS_ASSERT_OWNINGTHREAD(Promise); MaybeSomething(aArg, &Promise::MaybeReject); } void Promise::MaybeRejectWithUndefined() { NS_ASSERT_OWNINGTHREAD(Promise); MaybeSomething(JS::UndefinedHandleValue, &Promise::MaybeReject); } void Promise::ReportRejectedPromise(JSContext* aCx, JS::HandleObject aPromise) { MOZ_ASSERT(!js::IsWrapper(aPromise)); MOZ_ASSERT(JS::GetPromiseState(aPromise) == JS::PromiseState::Rejected); JS::Rooted result(aCx, JS::GetPromiseResult(aPromise)); js::ErrorReport report(aCx); if (!report.init(aCx, result, js::ErrorReport::NoSideEffects)) { JS_ClearPendingException(aCx); return; } RefPtr xpcReport = new xpc::ErrorReport(); bool isMainThread = MOZ_LIKELY(NS_IsMainThread()); bool isChrome = isMainThread ? nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(aPromise)) : IsCurrentThreadRunningChromeWorker(); nsGlobalWindowInner* win = isMainThread ? xpc::WindowGlobalOrNull(aPromise) : nullptr; xpcReport->Init(report.report(), report.toStringResult().c_str(), isChrome, win ? win->AsInner()->WindowID() : 0); // Now post an event to do the real reporting async RefPtr event = new AsyncErrorReporter(xpcReport); if (win) { win->Dispatch(mozilla::TaskCategory::Other, event.forget()); } else { NS_DispatchToMainThread(event); } } JSObject* Promise::GlobalJSObject() const { return mGlobal->GetGlobalJSObject(); } JSCompartment* Promise::Compartment() const { return js::GetObjectCompartment(GlobalJSObject()); } // A WorkerRunnable to resolve/reject the Promise on the worker thread. // Calling thread MUST hold PromiseWorkerProxy's mutex before creating this. class PromiseWorkerProxyRunnable : public WorkerRunnable { public: PromiseWorkerProxyRunnable(PromiseWorkerProxy* aPromiseWorkerProxy, PromiseWorkerProxy::RunCallbackFunc aFunc) : WorkerRunnable(aPromiseWorkerProxy->GetWorkerPrivate(), WorkerThreadUnchangedBusyCount) , mPromiseWorkerProxy(aPromiseWorkerProxy) , mFunc(aFunc) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mPromiseWorkerProxy); } virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); MOZ_ASSERT(mPromiseWorkerProxy); RefPtr workerPromise = mPromiseWorkerProxy->WorkerPromise(); // Here we convert the buffer to a JS::Value. JS::Rooted value(aCx); if (!mPromiseWorkerProxy->Read(aCx, &value)) { JS_ClearPendingException(aCx); return false; } (workerPromise->*mFunc)(aCx, value); // Release the Promise because it has been resolved/rejected for sure. mPromiseWorkerProxy->CleanUp(); return true; } protected: ~PromiseWorkerProxyRunnable() {} private: RefPtr mPromiseWorkerProxy; // Function pointer for calling Promise::{ResolveInternal,RejectInternal}. PromiseWorkerProxy::RunCallbackFunc mFunc; }; /* static */ already_AddRefed PromiseWorkerProxy::Create(WorkerPrivate* aWorkerPrivate, Promise* aWorkerPromise, const PromiseWorkerProxyStructuredCloneCallbacks* aCb) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aWorkerPromise); MOZ_ASSERT_IF(aCb, !!aCb->Write && !!aCb->Read); RefPtr proxy = new PromiseWorkerProxy(aWorkerPromise, aCb); // We do this to make sure the worker thread won't shut down before the // promise is resolved/rejected on the worker thread. RefPtr workerRef = StrongWorkerRef::Create(aWorkerPrivate, "PromiseWorkerProxy", [proxy]() { proxy->CleanUp(); }); if (NS_WARN_IF(!workerRef)) { // Probably the worker is terminating. We cannot complete the operation // and we have to release all the resources. proxy->CleanProperties(); return nullptr; } proxy->mWorkerRef = new ThreadSafeWorkerRef(workerRef); // Maintain a reference so that we have a valid object to clean up when // removing the feature. proxy.get()->AddRef(); return proxy.forget(); } NS_IMPL_ISUPPORTS0(PromiseWorkerProxy) PromiseWorkerProxy::PromiseWorkerProxy(Promise* aWorkerPromise, const PromiseWorkerProxyStructuredCloneCallbacks* aCallbacks) : mWorkerPromise(aWorkerPromise) , mCleanedUp(false) , mCallbacks(aCallbacks) , mCleanUpLock("cleanUpLock") { } PromiseWorkerProxy::~PromiseWorkerProxy() { MOZ_ASSERT(mCleanedUp); MOZ_ASSERT(!mWorkerPromise); MOZ_ASSERT(!mWorkerRef); } void PromiseWorkerProxy::CleanProperties() { MOZ_ASSERT(IsCurrentThreadRunningWorker()); // Ok to do this unprotected from Create(). // CleanUp() holds the lock before calling this. mCleanedUp = true; mWorkerPromise = nullptr; mWorkerRef = nullptr; // Clear the StructuredCloneHolderBase class. Clear(); } WorkerPrivate* PromiseWorkerProxy::GetWorkerPrivate() const { #ifdef DEBUG if (NS_IsMainThread()) { mCleanUpLock.AssertCurrentThreadOwns(); } #endif // Safe to check this without a lock since we assert lock ownership on the // main thread above. MOZ_ASSERT(!mCleanedUp); MOZ_ASSERT(mWorkerRef); return mWorkerRef->Private(); } Promise* PromiseWorkerProxy::WorkerPromise() const { MOZ_ASSERT(IsCurrentThreadRunningWorker()); MOZ_ASSERT(mWorkerPromise); return mWorkerPromise; } void PromiseWorkerProxy::RunCallback(JSContext* aCx, JS::Handle aValue, RunCallbackFunc aFunc) { MOZ_ASSERT(NS_IsMainThread()); MutexAutoLock lock(Lock()); // If the worker thread's been cancelled we don't need to resolve the Promise. if (CleanedUp()) { return; } // The |aValue| is written into the StructuredCloneHolderBase. if (!Write(aCx, aValue)) { JS_ClearPendingException(aCx); MOZ_ASSERT(false, "cannot serialize the value with the StructuredCloneAlgorithm!"); } RefPtr runnable = new PromiseWorkerProxyRunnable(this, aFunc); runnable->Dispatch(); } void PromiseWorkerProxy::ResolvedCallback(JSContext* aCx, JS::Handle aValue) { RunCallback(aCx, aValue, &Promise::MaybeResolve); } void PromiseWorkerProxy::RejectedCallback(JSContext* aCx, JS::Handle aValue) { RunCallback(aCx, aValue, &Promise::MaybeReject); } void PromiseWorkerProxy::CleanUp() { // Can't release Mutex while it is still locked, so scope the lock. { MutexAutoLock lock(Lock()); if (CleanedUp()) { return; } MOZ_ASSERT(mWorkerRef); mWorkerRef->Private()->AssertIsOnWorkerThread(); // Release the Promise and remove the PromiseWorkerProxy from the holders of // the worker thread since the Promise has been resolved/rejected or the // worker thread has been cancelled. mWorkerRef = nullptr; CleanProperties(); } Release(); } JSObject* PromiseWorkerProxy::CustomReadHandler(JSContext* aCx, JSStructuredCloneReader* aReader, uint32_t aTag, uint32_t aIndex) { if (NS_WARN_IF(!mCallbacks)) { return nullptr; } return mCallbacks->Read(aCx, aReader, this, aTag, aIndex); } bool PromiseWorkerProxy::CustomWriteHandler(JSContext* aCx, JSStructuredCloneWriter* aWriter, JS::Handle aObj) { if (NS_WARN_IF(!mCallbacks)) { return false; } return mCallbacks->Write(aCx, aWriter, this, aObj); } // Specializations of MaybeRejectBrokenly we actually support. template<> void Promise::MaybeRejectBrokenly(const RefPtr& aArg) { MaybeSomething(aArg, &Promise::MaybeReject); } template<> void Promise::MaybeRejectBrokenly(const nsAString& aArg) { MaybeSomething(aArg, &Promise::MaybeReject); } Promise::PromiseState Promise::State() const { JS::Rooted p(RootingCx(), PromiseObj()); const JS::PromiseState state = JS::GetPromiseState(p); if (state == JS::PromiseState::Fulfilled) { return PromiseState::Resolved; } if (state == JS::PromiseState::Rejected) { return PromiseState::Rejected; } return PromiseState::Pending; } } // namespace dom } // namespace mozilla