diff options
Diffstat (limited to '')
-rw-r--r-- | dom/ipc/jsactor/JSActor.cpp | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/dom/ipc/jsactor/JSActor.cpp b/dom/ipc/jsactor/JSActor.cpp new file mode 100644 index 0000000000..8ac0442a35 --- /dev/null +++ b/dom/ipc/jsactor/JSActor.cpp @@ -0,0 +1,503 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/JSActor.h" +#include "mozilla/dom/JSActorBinding.h" + +#include "chrome/common/ipc_channel.h" +#include "mozilla/Attributes.h" +#include "mozilla/FunctionRef.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ClonedErrorHolder.h" +#include "mozilla/dom/ClonedErrorHolderBinding.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "mozilla/dom/JSActorManager.h" +#include "mozilla/dom/MessageManagerBinding.h" +#include "mozilla/dom/PWindowGlobal.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ipc/StructuredCloneData.h" +#include "mozilla/ProfilerMarkers.h" +#include "js/Promise.h" +#include "xpcprivate.h" +#include "nsFrameMessageManager.h" +#include "nsICrashReporter.h" + +namespace mozilla::dom { + +struct JSActorMessageMarker { + static constexpr Span<const char> MarkerTypeName() { + return MakeStringSpan("JSActorMessage"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + const ProfilerString8View& aActorName, + const ProfilerString16View& aMessageName) { + aWriter.StringProperty("actor", aActorName); + aWriter.StringProperty("name", NS_ConvertUTF16toUTF8(aMessageName)); + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormatSearchable( + "actor", "Actor Name", MS::Format::String, MS::Searchable::Searchable); + schema.AddKeyLabelFormatSearchable( + "name", "Message Name", MS::Format::String, MS::Searchable::Searchable); + schema.SetTooltipLabel("JSActor - {marker.name}"); + schema.SetTableLabel( + "{marker.name} - [{marker.data.actor}] {marker.data.name}"); + return schema; + } +}; + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSActor) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(JSActor) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JSActor) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(JSActor) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(JSActor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWrappedJS) + tmp->mPendingQueries.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(JSActor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWrappedJS) + for (const auto& query : tmp->mPendingQueries.Values()) { + CycleCollectionNoteChild(cb, query.mPromise.get(), "Pending Query Promise"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +JSActor::JSActor(nsISupports* aGlobal) { + mGlobal = do_QueryInterface(aGlobal); + if (!mGlobal) { + mGlobal = xpc::NativeGlobal(xpc::PrivilegedJunkScope()); + } +} + +void JSActor::StartDestroy() { mCanSend = false; } + +void JSActor::AfterDestroy() { + mCanSend = false; + + // Take our queries out, in case somehow rejecting promises can trigger + // additions or removals. + const nsTHashMap<nsUint64HashKey, PendingQuery> pendingQueries = + std::move(mPendingQueries); + for (const auto& entry : pendingQueries.Values()) { + nsPrintfCString message( + "Actor '%s' destroyed before query '%s' was resolved", mName.get(), + NS_LossyConvertUTF16toASCII(entry.mMessageName).get()); + entry.mPromise->MaybeRejectWithAbortError(message); + } + + InvokeCallback(CallbackFunction::DidDestroy); + ClearManager(); +} + +void JSActor::InvokeCallback(CallbackFunction callback) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + + AutoEntryScript aes(GetParentObject(), "JSActor destroy callback"); + JSContext* cx = aes.cx(); + MozJSActorCallbacks callbacksHolder; + JS::Rooted<JS::Value> val(cx, JS::ObjectOrNullValue(GetWrapper())); + if (NS_WARN_IF(!callbacksHolder.Init(cx, val))) { + return; + } + + // Destroy callback is optional. + if (callback == CallbackFunction::DidDestroy) { + if (callbacksHolder.mDidDestroy.WasPassed()) { + callbacksHolder.mDidDestroy.Value()->Call(this); + } + } else { + if (callbacksHolder.mActorCreated.WasPassed()) { + callbacksHolder.mActorCreated.Value()->Call(this); + } + } +} + +nsresult JSActor::QueryInterfaceActor(const nsIID& aIID, void** aPtr) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + if (!GetWrapperPreserveColor()) { + // If we have no preserved wrapper, we won't implement any interfaces. + return NS_NOINTERFACE; + } + + if (!mWrappedJS) { + AutoEntryScript aes(GetParentObject(), "JSActor query interface"); + JSContext* cx = aes.cx(); + + JS::Rooted<JSObject*> self(cx, GetWrapper()); + JSAutoRealm ar(cx, self); + + RefPtr<nsXPCWrappedJS> wrappedJS; + nsresult rv = nsXPCWrappedJS::GetNewOrUsed( + cx, self, NS_GET_IID(nsISupports), getter_AddRefs(wrappedJS)); + NS_ENSURE_SUCCESS(rv, rv); + + mWrappedJS = do_QueryInterface(wrappedJS); + MOZ_ASSERT(mWrappedJS); + } + + return mWrappedJS->QueryInterface(aIID, aPtr); +} + +void JSActor::SetName(const nsACString& aName) { + MOZ_ASSERT(mName.IsEmpty(), "Cannot set name twice!"); + mName = aName; +} + +void JSActor::ThrowStateErrorForGetter(const char* aName, + ErrorResult& aRv) const { + if (mName.IsEmpty()) { + aRv.ThrowInvalidStateError(nsPrintfCString( + "Cannot access property '%s' before actor is initialized", aName)); + } else { + aRv.ThrowInvalidStateError(nsPrintfCString( + "Cannot access property '%s' after actor '%s' has been destroyed", + aName, mName.get())); + } +} + +static Maybe<ipc::StructuredCloneData> TryClone(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + Maybe<ipc::StructuredCloneData> data{std::in_place}; + + // Try to directly serialize the passed-in data, and return it to our caller. + IgnoredErrorResult rv; + data->Write(aCx, aValue, rv); + if (rv.Failed()) { + // Serialization failed, return `Nothing()` instead. + JS_ClearPendingException(aCx); + data.reset(); + } + return data; +} + +static Maybe<ipc::StructuredCloneData> CloneJSStack( + JSContext* aCx, JS::Handle<JSObject*> aStack) { + JS::Rooted<JS::Value> stackVal(aCx, JS::ObjectOrNullValue(aStack)); + return TryClone(aCx, stackVal); +} + +static Maybe<ipc::StructuredCloneData> CaptureJSStack(JSContext* aCx) { + JS::Rooted<JSObject*> stack(aCx, nullptr); + if (JS::IsAsyncStackCaptureEnabledForRealm(aCx) && + !JS::CaptureCurrentStack(aCx, &stack)) { + JS_ClearPendingException(aCx); + } + + return CloneJSStack(aCx, stack); +} + +void JSActor::SendAsyncMessage(JSContext* aCx, const nsAString& aMessageName, + JS::Handle<JS::Value> aObj, ErrorResult& aRv) { + profiler_add_marker("SendAsyncMessage", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMessageName); + Maybe<ipc::StructuredCloneData> data{std::in_place}; + if (!nsFrameMessageManager::GetParamsForMessage( + aCx, aObj, JS::UndefinedHandleValue, *data)) { + aRv.ThrowDataCloneError(nsPrintfCString( + "Failed to serialize message '%s::%s'", + NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); + return; + } + + JSActorMessageMeta meta; + meta.actorName() = mName; + meta.messageName() = aMessageName; + meta.kind() = JSActorMessageKind::Message; + + SendRawMessage(meta, std::move(data), CaptureJSStack(aCx), aRv); +} + +already_AddRefed<Promise> JSActor::SendQuery(JSContext* aCx, + const nsAString& aMessageName, + JS::Handle<JS::Value> aObj, + ErrorResult& aRv) { + profiler_add_marker("SendQuery", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMessageName); + Maybe<ipc::StructuredCloneData> data{std::in_place}; + if (!nsFrameMessageManager::GetParamsForMessage( + aCx, aObj, JS::UndefinedHandleValue, *data)) { + aRv.ThrowDataCloneError(nsPrintfCString( + "Failed to serialize message '%s::%s'", + NS_LossyConvertUTF16toASCII(aMessageName).get(), mName.get())); + return nullptr; + } + + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + aRv.ThrowUnknownError("Unable to get current native global"); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + JSActorMessageMeta meta; + meta.actorName() = mName; + meta.messageName() = aMessageName; + meta.queryId() = mNextQueryId++; + meta.kind() = JSActorMessageKind::Query; + + mPendingQueries.InsertOrUpdate(meta.queryId(), + PendingQuery{promise, meta.messageName()}); + + SendRawMessage(meta, std::move(data), CaptureJSStack(aCx), aRv); + return promise.forget(); +} + +void JSActor::CallReceiveMessage(JSContext* aCx, + const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) { + // The argument which we want to pass to IPC. + RootedDictionary<ReceiveMessageArgument> argument(aCx); + argument.mTarget = this; + argument.mName = aMetadata.messageName(); + argument.mData = aData; + argument.mJson = aData; + argument.mSync = false; + + if (GetWrapperPreserveColor()) { + // Invoke the actual callback. + JS::Rooted<JSObject*> global(aCx, JS::GetNonCCWObjectGlobal(GetWrapper())); + RefPtr<MessageListener> messageListener = + new MessageListener(GetWrapper(), global, nullptr, nullptr); + messageListener->ReceiveMessage(argument, aRetVal, aRv, + "JSActor receive message", + MessageListener::eRethrowExceptions); + } else { + aRv.ThrowTypeError<MSG_NOT_CALLABLE>("Property 'receiveMessage'"); + } +} + +void JSActor::ReceiveMessage(JSContext* aCx, + const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv) { + MOZ_ASSERT(aMetadata.kind() == JSActorMessageKind::Message); + profiler_add_marker("ReceiveMessage", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMetadata.messageName()); + + JS::Rooted<JS::Value> retval(aCx); + CallReceiveMessage(aCx, aMetadata, aData, &retval, aRv); +} + +void JSActor::ReceiveQuery(JSContext* aCx, const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv) { + MOZ_ASSERT(aMetadata.kind() == JSActorMessageKind::Query); + profiler_add_marker("ReceiveQuery", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMetadata.messageName()); + + // This promise will be resolved or rejected once the listener has been + // called. Our listener on this promise will then send the reply. + RefPtr<Promise> promise = Promise::Create(GetParentObject(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RefPtr<QueryHandler> handler = new QueryHandler(this, aMetadata, promise); + promise->AppendNativeHandler(handler); + + ErrorResult error; + JS::Rooted<JS::Value> retval(aCx); + CallReceiveMessage(aCx, aMetadata, aData, &retval, error); + + // If we have a promise, resolve or reject it respectively. + if (error.Failed()) { + if (error.IsUncatchableException()) { + promise->MaybeRejectWithTimeoutError( + "Message handler threw uncatchable exception"); + } else { + promise->MaybeReject(std::move(error)); + } + } else { + promise->MaybeResolve(retval); + } + error.SuppressException(); +} + +void JSActor::ReceiveQueryReply(JSContext* aCx, + const JSActorMessageMeta& aMetadata, + JS::Handle<JS::Value> aData, ErrorResult& aRv) { + if (NS_WARN_IF(aMetadata.actorName() != mName)) { + aRv.ThrowUnknownError("Mismatched actor name for query reply"); + return; + } + + Maybe<PendingQuery> query = mPendingQueries.Extract(aMetadata.queryId()); + if (NS_WARN_IF(!query)) { + aRv.ThrowUnknownError("Received reply for non-pending query"); + return; + } + + profiler_add_marker("ReceiveQueryReply", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mName, aMetadata.messageName()); + + Promise* promise = query->mPromise; + JSAutoRealm ar(aCx, promise->PromiseObj()); + JS::RootedValue data(aCx, aData); + if (NS_WARN_IF(!JS_WrapValue(aCx, &data))) { + aRv.NoteJSContextException(aCx); + return; + } + + if (aMetadata.kind() == JSActorMessageKind::QueryResolve) { + promise->MaybeResolve(data); + } else { + promise->MaybeReject(data); + } +} + +void JSActor::SendRawMessageInProcess(const JSActorMessageMeta& aMeta, + Maybe<ipc::StructuredCloneData>&& aData, + Maybe<ipc::StructuredCloneData>&& aStack, + OtherSideCallback&& aGetOtherSide) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "JSActor Async Message", + [aMeta, data{std::move(aData)}, stack{std::move(aStack)}, + getOtherSide{std::move(aGetOtherSide)}]() mutable { + if (RefPtr<JSActorManager> otherSide = getOtherSide()) { + otherSide->ReceiveRawMessage(aMeta, std::move(data), + std::move(stack)); + } + })); +} + +// Native handler for our generated promise which is used to handle Queries and +// send the reply when their promises have been resolved. +JSActor::QueryHandler::QueryHandler(JSActor* aActor, + const JSActorMessageMeta& aMetadata, + Promise* aPromise) + : mActor(aActor), + mPromise(aPromise), + mMessageName(aMetadata.messageName()), + mQueryId(aMetadata.queryId()) {} + +void JSActor::QueryHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!mActor) { + // Make sure that this rejection is reported. See comment below. + if (!JS::CallOriginalPromiseReject(aCx, aValue)) { + JS_ClearPendingException(aCx); + } + return; + } + + JS::Rooted<JS::Value> value(aCx, aValue); + if (value.isObject()) { + JS::Rooted<JSObject*> error(aCx, &value.toObject()); + if (RefPtr<ClonedErrorHolder> ceh = + ClonedErrorHolder::Create(aCx, error, IgnoreErrors())) { + JS::RootedObject obj(aCx); + // Note: We can't use `ToJSValue` here because ClonedErrorHolder isn't + // wrapper cached. + if (ceh->WrapObject(aCx, nullptr, &obj)) { + value.setObject(*obj); + } else { + JS_ClearPendingException(aCx); + } + } else { + JS_ClearPendingException(aCx); + } + } + + Maybe<ipc::StructuredCloneData> data = TryClone(aCx, value); + if (!data) { + // Failed to clone the rejection value. Make sure that this + // rejection is reported, despite being "handled". This is done by + // creating a new promise in the rejected state, and throwing it + // away. This will be reported as an unhandled rejected promise. + if (!JS::CallOriginalPromiseReject(aCx, aValue)) { + JS_ClearPendingException(aCx); + } + } + + SendReply(aCx, JSActorMessageKind::QueryReject, std::move(data)); +} + +void JSActor::QueryHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (!mActor) { + return; + } + + Maybe<ipc::StructuredCloneData> data{std::in_place}; + data->InitScope(JS::StructuredCloneScope::DifferentProcess); + + IgnoredErrorResult error; + data->Write(aCx, aValue, error); + if (NS_WARN_IF(error.Failed())) { + JS_ClearPendingException(aCx); + + nsAutoCString msg; + msg.Append(mActor->Name()); + msg.Append(':'); + msg.Append(NS_LossyConvertUTF16toASCII(mMessageName)); + msg.AppendLiteral(": message reply cannot be cloned."); + + auto exc = MakeRefPtr<Exception>(msg, NS_ERROR_FAILURE, "DataCloneError"_ns, + nullptr, nullptr); + + JS::Rooted<JS::Value> val(aCx); + if (ToJSValue(aCx, exc, &val)) { + RejectedCallback(aCx, val, aRv); + } else { + JS_ClearPendingException(aCx); + } + return; + } + + SendReply(aCx, JSActorMessageKind::QueryResolve, std::move(data)); +} + +void JSActor::QueryHandler::SendReply(JSContext* aCx, JSActorMessageKind aKind, + Maybe<ipc::StructuredCloneData>&& aData) { + MOZ_ASSERT(mActor); + profiler_add_marker("SendQueryReply", geckoprofiler::category::IPC, {}, + JSActorMessageMarker{}, mActor->Name(), mMessageName); + + JSActorMessageMeta meta; + meta.actorName() = mActor->Name(); + meta.messageName() = mMessageName; + meta.queryId() = mQueryId; + meta.kind() = aKind; + + JS::Rooted<JSObject*> promise(aCx, mPromise->PromiseObj()); + JS::Rooted<JSObject*> stack(aCx, JS::GetPromiseResolutionSite(promise)); + + mActor->SendRawMessage(meta, std::move(aData), CloneJSStack(aCx, stack), + IgnoreErrors()); + mActor = nullptr; + mPromise = nullptr; +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSActor::QueryHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(JSActor::QueryHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JSActor::QueryHandler) + +NS_IMPL_CYCLE_COLLECTION(JSActor::QueryHandler, mActor, mPromise) + +} // namespace mozilla::dom |