/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "RemoteWorkerManager.h" #include #include "mozilla/SchedulerGroup.h" #include "mozilla/ScopeExit.h" #include "mozilla/dom/ContentChild.h" // ContentChild::GetSingleton #include "mozilla/dom/RemoteWorkerController.h" #include "mozilla/dom/RemoteWorkerParent.h" #include "mozilla/ipc/BackgroundParent.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/ipc/PBackgroundParent.h" #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED # include "mozilla/dom/DOMException.h" # include "mozilla/CycleCollectedJSContext.h" # include "mozilla/Sprintf.h" // SprintfLiteral # include "nsIXPConnect.h" // nsIXPConnectWrappedJS #endif #include "mozilla/StaticPrefs_extensions.h" #include "nsCOMPtr.h" #include "nsIE10SUtils.h" #include "nsImportModule.h" #include "nsIXULRuntime.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "RemoteWorkerServiceParent.h" mozilla::LazyLogModule gRemoteWorkerManagerLog("RemoteWorkerManager"); #ifdef LOG # undef LOG #endif #define LOG(fmt) \ MOZ_LOG(gRemoteWorkerManagerLog, mozilla::LogLevel::Verbose, fmt) namespace mozilla { using namespace ipc; namespace dom { namespace { // Raw pointer because this object is kept alive by RemoteWorkerServiceParent // actors. RemoteWorkerManager* sRemoteWorkerManager; bool IsServiceWorker(const RemoteWorkerData& aData) { return aData.serviceWorkerData().type() == OptionalServiceWorkerData::TServiceWorkerData; } void TransmitPermissionsAndBlobURLsForPrincipalInfo( ContentParent* aContentParent, const PrincipalInfo& aPrincipalInfo) { AssertIsOnMainThread(); MOZ_ASSERT(aContentParent); auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo); if (NS_WARN_IF(principalOrErr.isErr())) { return; } nsCOMPtr principal = principalOrErr.unwrap(); aContentParent->TransmitBlobURLsForPrincipal(principal); MOZ_ALWAYS_SUCCEEDS( aContentParent->TransmitPermissionsForPrincipal(principal)); } } // namespace // static bool RemoteWorkerManager::MatchRemoteType(const nsACString& processRemoteType, const nsACString& workerRemoteType) { LOG(("MatchRemoteType [processRemoteType=%s, workerRemoteType=%s]", PromiseFlatCString(processRemoteType).get(), PromiseFlatCString(workerRemoteType).get())); // Respecting COOP and COEP requires processing headers in the parent // process in order to choose an appropriate content process, but the // workers' ScriptLoader processes headers in content processes. An // intermediary step that provides security guarantees is to simply never // allow SharedWorkers and ServiceWorkers to exist in a COOP+COEP process. // The ultimate goal is to allow these worker types to be put in such // processes based on their script response headers. // https://bugzilla.mozilla.org/show_bug.cgi?id=1595206 // // RemoteWorkerManager::GetRemoteType should not select this remoteType // and so workerRemoteType is not expected to be set to a coop+coep // remoteType and here we can just assert that it is not happening. MOZ_ASSERT(!IsWebCoopCoepRemoteType(workerRemoteType)); return processRemoteType.Equals(workerRemoteType); } // static Result RemoteWorkerManager::GetRemoteType( const nsCOMPtr& aPrincipal, WorkerKind aWorkerKind) { AssertIsOnMainThread(); MOZ_ASSERT_IF(aWorkerKind == WorkerKind::WorkerKindService, aPrincipal->GetIsContentPrincipal()); nsCOMPtr e10sUtils = do_ImportESModule( "resource://gre/modules/E10SUtils.sys.mjs", "E10SUtils", fallible); if (NS_WARN_IF(!e10sUtils)) { LOG(("GetRemoteType Abort: could not import E10SUtils")); return Err(NS_ERROR_DOM_ABORT_ERR); } nsCString preferredRemoteType = DEFAULT_REMOTE_TYPE; if (aWorkerKind == WorkerKind::WorkerKindShared) { if (auto* contentChild = ContentChild::GetSingleton()) { // For a shared worker set the preferred remote type to the content // child process remote type. preferredRemoteType = contentChild->GetRemoteType(); } else if (aPrincipal->IsSystemPrincipal()) { preferredRemoteType = NOT_REMOTE_TYPE; } } nsIE10SUtils::RemoteWorkerType workerType; switch (aWorkerKind) { case WorkerKind::WorkerKindService: workerType = nsIE10SUtils::REMOTE_WORKER_TYPE_SERVICE; break; case WorkerKind::WorkerKindShared: workerType = nsIE10SUtils::REMOTE_WORKER_TYPE_SHARED; break; default: // This method isn't expected to be called for worker types that // aren't remote workers (currently Service and Shared workers). LOG(("GetRemoteType Error on unexpected worker type")); MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected worker type"); return Err(NS_ERROR_DOM_ABORT_ERR); } // Here we do not have access to the window and so we can't use its // useRemoteTabs and useRemoteSubframes flags (for the service // worker there may not even be a window associated to the worker // yet), and so we have to use the prefs instead. bool isMultiprocess = BrowserTabsRemoteAutostart(); bool isFission = FissionAutostart(); nsCString remoteType = NOT_REMOTE_TYPE; nsresult rv = e10sUtils->GetRemoteTypeForWorkerPrincipal( aPrincipal, workerType, isMultiprocess, isFission, preferredRemoteType, remoteType); if (NS_WARN_IF(NS_FAILED(rv))) { LOG( ("GetRemoteType Abort: E10SUtils.getRemoteTypeForWorkerPrincipal " "exception")); #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED nsCString principalTypeOrScheme; if (aPrincipal->IsSystemPrincipal()) { principalTypeOrScheme = "system"_ns; } else if (aPrincipal->GetIsExpandedPrincipal()) { principalTypeOrScheme = "expanded"_ns; } else if (aPrincipal->GetIsNullPrincipal()) { principalTypeOrScheme = "null"_ns; } else { nsCOMPtr uri = aPrincipal->GetURI(); nsresult rv2 = uri->GetScheme(principalTypeOrScheme); if (NS_FAILED(rv2)) { principalTypeOrScheme = "content"_ns; } } nsCString processRemoteType = "parent"_ns; if (auto* contentChild = ContentChild::GetSingleton()) { // RemoteTypePrefix make sure that we are not going to include // the full origin that may be part of the current remote type. processRemoteType = RemoteTypePrefix(contentChild->GetRemoteType()); } // Convert the error code into an error name. nsAutoCString errorName; GetErrorName(rv, errorName); // Try to retrieve the line number from the exception. nsAutoCString errorFilename("(unknown)"_ns); uint32_t jsmErrorLineNumber = 0; if (auto* context = CycleCollectedJSContext::Get()) { if (RefPtr exn = context->GetPendingException()) { nsAutoString filename(u"(unknown)"_ns); if (rv == NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS) { // When the failure is a Javascript Error, the line number retrieved // from the Exception instance isn't going to be the E10SUtils.sys.mjs // line that originated the failure, and so we fallback to retrieve it // from the nsIScriptError. nsCOMPtr scriptError = do_QueryInterface(exn->GetData()); if (scriptError) { scriptError->GetLineNumber(&jsmErrorLineNumber); scriptError->GetSourceName(filename); } } else { nsCOMPtr wrapped = do_QueryInterface(e10sUtils); dom::AutoJSAPI jsapi; if (jsapi.Init(wrapped->GetJSObjectGlobal())) { auto* cx = jsapi.cx(); jsmErrorLineNumber = exn->LineNumber(cx); exn->GetFilename(cx, filename); } } errorFilename = NS_ConvertUTF16toUTF8(filename); } } char buf[1024]; SprintfLiteral( buf, "workerType=%s, principal=%s, preferredRemoteType=%s, " "processRemoteType=%s, errorName=%s, errorLocation=%s:%d", aWorkerKind == WorkerKind::WorkerKindService ? "service" : "shared", principalTypeOrScheme.get(), PromiseFlatCString(RemoteTypePrefix(preferredRemoteType)).get(), processRemoteType.get(), errorName.get(), errorFilename.get(), jsmErrorLineNumber); MOZ_CRASH_UNSAFE_PRINTF( "E10SUtils.getRemoteTypeForWorkerPrincipal did throw: %s", buf); #endif return Err(NS_ERROR_DOM_ABORT_ERR); } if (MOZ_LOG_TEST(gRemoteWorkerManagerLog, LogLevel::Verbose)) { nsCString principalOrigin; aPrincipal->GetOrigin(principalOrigin); LOG( ("GetRemoteType workerType=%s, principal=%s, " "preferredRemoteType=%s, selectedRemoteType=%s", aWorkerKind == WorkerKind::WorkerKindService ? "service" : "shared", principalOrigin.get(), preferredRemoteType.get(), remoteType.get())); } return remoteType; } // static bool RemoteWorkerManager::HasExtensionPrincipal(const RemoteWorkerData& aData) { auto principalInfo = aData.principalInfo(); return principalInfo.type() == PrincipalInfo::TContentPrincipalInfo && // This helper method is also called from the background thread and so // we can't check if the principal does have an addonPolicy object // associated and we have to resort to check the url scheme instead. StringBeginsWith(principalInfo.get_ContentPrincipalInfo().spec(), "moz-extension://"_ns); } // static bool RemoteWorkerManager::IsRemoteTypeAllowed(const RemoteWorkerData& aData) { AssertIsOnMainThread(); // If Gecko is running in single process mode, there is no child process // to select and we have to just consider it valid (if it should haven't // been launched it should have been already prevented before reaching // a RemoteWorkerChild instance). if (!BrowserTabsRemoteAutostart()) { return true; } const auto& principalInfo = aData.principalInfo(); auto* contentChild = ContentChild::GetSingleton(); if (!contentChild) { // If e10s isn't disabled, only workers related to the system principal // should be allowed to run in the parent process, and extension principals // if extensions.webextensions.remote is false. return principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || (!StaticPrefs::extensions_webextensions_remote() && aData.remoteType().Equals(NOT_REMOTE_TYPE) && HasExtensionPrincipal(aData)); } auto principalOrErr = PrincipalInfoToPrincipal(principalInfo); if (NS_WARN_IF(principalOrErr.isErr())) { return false; } nsCOMPtr principal = principalOrErr.unwrap(); // Recompute the remoteType based on the principal, to double-check that it // has not been tempered to select a different child process than the one // expected. bool isServiceWorker = aData.serviceWorkerData().type() == OptionalServiceWorkerData::TServiceWorkerData; auto remoteType = GetRemoteType( principal, isServiceWorker ? WorkerKindService : WorkerKindShared); if (NS_WARN_IF(remoteType.isErr())) { LOG(("IsRemoteTypeAllowed: Error to retrieve remote type")); return false; } return MatchRemoteType(remoteType.unwrap(), contentChild->GetRemoteType()); } /* static */ already_AddRefed RemoteWorkerManager::GetOrCreate() { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); if (!sRemoteWorkerManager) { sRemoteWorkerManager = new RemoteWorkerManager(); } RefPtr rwm = sRemoteWorkerManager; return rwm.forget(); } RemoteWorkerManager::RemoteWorkerManager() : mParentActor(nullptr) { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); MOZ_ASSERT(!sRemoteWorkerManager); } RemoteWorkerManager::~RemoteWorkerManager() { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); MOZ_ASSERT(sRemoteWorkerManager == this); sRemoteWorkerManager = nullptr; } void RemoteWorkerManager::RegisterActor(RemoteWorkerServiceParent* aActor) { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); if (!BackgroundParent::IsOtherProcessActor(aActor->Manager())) { MOZ_ASSERT(!mParentActor); mParentActor = aActor; MOZ_ASSERT(mPendings.IsEmpty()); return; } MOZ_ASSERT(!mChildActors.Contains(aActor)); mChildActors.AppendElement(aActor); if (!mPendings.IsEmpty()) { const auto& processRemoteType = aActor->GetRemoteType(); nsTArray unlaunched; // Flush pending launching. for (Pending& p : mPendings) { if (p.mController->IsTerminated()) { continue; } const auto& workerRemoteType = p.mData.remoteType(); if (MatchRemoteType(processRemoteType, workerRemoteType)) { LOG(("RegisterActor - Launch Pending, workerRemoteType=%s", workerRemoteType.get())); LaunchInternal(p.mController, aActor, p.mData); } else { unlaunched.AppendElement(std::move(p)); continue; } } std::swap(mPendings, unlaunched); // AddRef is called when the first Pending object is added to mPendings, so // the balancing Release is called when the last Pending object is removed. // RemoteWorkerServiceParents will hold strong references to // RemoteWorkerManager. if (mPendings.IsEmpty()) { Release(); } LOG(("RegisterActor - mPendings length: %zu", mPendings.Length())); } } void RemoteWorkerManager::UnregisterActor(RemoteWorkerServiceParent* aActor) { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); if (aActor == mParentActor) { mParentActor = nullptr; } else { MOZ_ASSERT(mChildActors.Contains(aActor)); mChildActors.RemoveElement(aActor); } } void RemoteWorkerManager::Launch(RemoteWorkerController* aController, const RemoteWorkerData& aData, base::ProcessId aProcessId) { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); RemoteWorkerServiceParent* targetActor = SelectTargetActor(aData, aProcessId); // If there is not an available actor, let's store the data, and let's spawn a // new process. if (!targetActor) { // If this is the first time we have a pending launching, we must keep alive // the manager. if (mPendings.IsEmpty()) { AddRef(); } Pending* pending = mPendings.AppendElement(); pending->mController = aController; pending->mData = aData; // Launching is async, so we cannot check for failures right here. LaunchNewContentProcess(aData); return; } /** * If a target actor for the remote worker has been selected, the actor has * already been registered with the corresponding `ContentParent` and we * should not increment the `mRemoteWorkerActorData`'s `mCount` again (see * `SelectTargetActorForServiceWorker()` / * `SelectTargetActorForSharedWorker()`). */ LaunchInternal(aController, targetActor, aData, true); } void RemoteWorkerManager::LaunchInternal( RemoteWorkerController* aController, RemoteWorkerServiceParent* aTargetActor, const RemoteWorkerData& aData, bool aRemoteWorkerAlreadyRegistered) { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); MOZ_ASSERT(aController); MOZ_ASSERT(aTargetActor); MOZ_ASSERT(aTargetActor == mParentActor || mChildActors.Contains(aTargetActor)); // We need to send permissions to content processes, but not if we're spawning // the worker here in the parent process. if (aTargetActor != mParentActor) { RefPtr contentHandle = BackgroundParent::GetContentParentHandle(aTargetActor->Manager()); // This won't cause any race conditions because the content process // should wait for the permissions to be received before executing the // Service Worker. nsCOMPtr r = NS_NewRunnableFunction( __func__, [contentHandle = std::move(contentHandle), principalInfo = aData.principalInfo()] { AssertIsOnMainThread(); if (RefPtr contentParent = contentHandle->GetContentParent()) { TransmitPermissionsAndBlobURLsForPrincipalInfo(contentParent, principalInfo); } }); MOZ_ALWAYS_SUCCEEDS( SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); } RefPtr workerActor = MakeAndAddRef(); if (!aTargetActor->Manager()->SendPRemoteWorkerConstructor(workerActor, aData)) { AsyncCreationFailed(aController); return; } workerActor->Initialize(aRemoteWorkerAlreadyRegistered); // This makes the link better the 2 actors. aController->SetWorkerActor(workerActor); workerActor->SetController(aController); } void RemoteWorkerManager::AsyncCreationFailed( RemoteWorkerController* aController) { RefPtr controller = aController; nsCOMPtr r = NS_NewRunnableFunction("RemoteWorkerManager::AsyncCreationFailed", [controller]() { controller->CreationFailed(); }); NS_DispatchToCurrentThread(r.forget()); } template void RemoteWorkerManager::ForEachActor( Callback&& aCallback, const nsACString& aRemoteType, Maybe aProcessId) const { AssertIsOnBackgroundThread(); const auto length = mChildActors.Length(); auto end = static_cast(rand()) % length; if (aProcessId) { // Start from the actor with the given processId instead of starting from // a random index. for (auto j = length - 1; j > 0; j--) { if (mChildActors[j]->OtherPid() == *aProcessId) { end = j; break; } } } uint32_t i = end; do { MOZ_ASSERT(i < mChildActors.Length()); RemoteWorkerServiceParent* actor = mChildActors[i]; if (MatchRemoteType(actor->GetRemoteType(), aRemoteType)) { ThreadsafeContentParentHandle* contentHandle = BackgroundParent::GetContentParentHandle(actor->Manager()); if (!aCallback(actor, contentHandle)) { break; } } i = (i + 1) % length; } while (i != end); } /** * When selecting a target actor for a given remote worker, we have to consider * that: * * - Service Workers can spawn even when their registering page/script isn't * active (e.g. push notifications), so we don't attempt to spawn the worker * in its registering script's process. We search linearly and choose the * search's starting position randomly. * * - When Fission is enabled, Shared Workers may have to be spawned into * different child process from the one where it has been registered from, and * that child process may be going to be marked as dead and shutdown. * * Spawning the workers in a random process makes the process selection criteria * a little tricky, as a candidate process may imminently shutdown due to a * remove worker actor unregistering * (see `ContentParent::UnregisterRemoveWorkerActor`). * * In `ContentParent::MaybeAsyncSendShutDownMessage` we only dispatch a runnable * to call `ContentParent::ShutDownProcess` if there are no registered remote * worker actors, and we ensure that the check for the number of registered * actors and the dispatching of the runnable are atomic. That happens on the * main thread, so here on the background thread, while * `ContentParent::mRemoteWorkerActorData` is locked, if `mCount` > 0, we can * register a remote worker actor "early" and guarantee that the corresponding * content process will not shutdown. */ RemoteWorkerServiceParent* RemoteWorkerManager::SelectTargetActorInternal( const RemoteWorkerData& aData, base::ProcessId aProcessId) const { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mChildActors.IsEmpty()); RemoteWorkerServiceParent* actor = nullptr; const auto& workerRemoteType = aData.remoteType(); ForEachActor( [&](RemoteWorkerServiceParent* aActor, ThreadsafeContentParentHandle* aContentHandle) { // Make sure to choose an actor related to a child process that is not // going to shutdown while we are still in the process of launching the // remote worker. // // ForEachActor will start from the child actor coming from the child // process with a pid equal to aProcessId if any, otherwise it would // start from a random actor in the mChildActors array, this guarantees // that we will choose that actor if it does also match the remote type. if (aContentHandle->MaybeRegisterRemoteWorkerActor( [&](uint32_t count, bool shutdownStarted) -> bool { return (count || !shutdownStarted) && (aActor->OtherPid() == aProcessId || !actor); })) { actor = aActor; return false; } MOZ_ASSERT(!actor); return true; }, workerRemoteType, IsServiceWorker(aData) ? Nothing() : Some(aProcessId)); return actor; } RemoteWorkerServiceParent* RemoteWorkerManager::SelectTargetActor( const RemoteWorkerData& aData, base::ProcessId aProcessId) { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); // System principal workers should run on the parent process. if (aData.principalInfo().type() == PrincipalInfo::TSystemPrincipalInfo) { MOZ_ASSERT(mParentActor); return mParentActor; } // Extension principal workers are allowed to run on the parent process // when "extension.webextensions.remote" pref is false. if (aProcessId == base::GetCurrentProcId() && aData.remoteType().Equals(NOT_REMOTE_TYPE) && !StaticPrefs::extensions_webextensions_remote() && HasExtensionPrincipal(aData)) { MOZ_ASSERT(mParentActor); return mParentActor; } // If e10s is off, use the parent process. if (!BrowserTabsRemoteAutostart()) { MOZ_ASSERT(mParentActor); return mParentActor; } // We shouldn't have to worry about content-principal parent-process workers. MOZ_ASSERT(aProcessId != base::GetCurrentProcId()); if (mChildActors.IsEmpty()) { return nullptr; } return SelectTargetActorInternal(aData, aProcessId); } void RemoteWorkerManager::LaunchNewContentProcess( const RemoteWorkerData& aData) { AssertIsInMainProcess(); AssertIsOnBackgroundThread(); nsCOMPtr bgEventTarget = GetCurrentSerialEventTarget(); using LaunchPromiseType = ContentParent::LaunchPromise; using CallbackParamType = LaunchPromiseType::ResolveOrRejectValue; // A new content process must be requested on the main thread. On success, // the success callback will also run on the main thread. On failure, however, // the failure callback must be run on the background thread - it uses // RemoteWorkerManager, and RemoteWorkerManager isn't threadsafe, so the // promise callback will just dispatch the "real" failure callback to the // background thread. auto processLaunchCallback = [principalInfo = aData.principalInfo(), bgEventTarget = std::move(bgEventTarget), self = RefPtr(this)]( const CallbackParamType& aValue, const nsCString& remoteType) mutable { if (aValue.IsResolve()) { LOG(("LaunchNewContentProcess: successfully got child process")); // The failure callback won't run, and we're on the main thread, so // we need to properly release the thread-unsafe RemoteWorkerManager. NS_ProxyRelease(__func__, bgEventTarget, self.forget()); } else { // The "real" failure callback. nsCOMPtr r = NS_NewRunnableFunction( __func__, [self = std::move(self), remoteType] { nsTArray uncancelled; auto pendings = std::move(self->mPendings); for (const auto& pending : pendings) { const auto& workerRemoteType = pending.mData.remoteType(); if (self->MatchRemoteType(remoteType, workerRemoteType)) { LOG( ("LaunchNewContentProcess: Cancel pending with " "workerRemoteType=%s", workerRemoteType.get())); pending.mController->CreationFailed(); } else { uncancelled.AppendElement(pending); } } std::swap(self->mPendings, uncancelled); }); bgEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL); } }; LOG(("LaunchNewContentProcess: remoteType=%s", aData.remoteType().get())); nsCOMPtr r = NS_NewRunnableFunction( __func__, [callback = std::move(processLaunchCallback), workerRemoteType = aData.remoteType()]() mutable { auto remoteType = workerRemoteType.IsEmpty() ? DEFAULT_REMOTE_TYPE : workerRemoteType; RefPtr onFinished; if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { // Request a process making sure to specify aPreferUsed=true. For a // given remoteType there's a pool size limit. If we pass aPreferUsed // here, then if there's any process in the pool already, we will use // that. If we pass false (which is the default if omitted), then // this call will spawn a new process if the pool isn't at its limit // yet. // // (Our intent is never to grow the pool size here. Our logic gets // here because our current logic on PBackground is only aware of // RemoteWorkerServiceParent actors that have registered themselves, // which is fundamentally unaware of processes that will match in the // future when they register. So we absolutely are fine with and want // any existing processes.) onFinished = ContentParent::GetNewOrUsedBrowserProcessAsync( /* aRemoteType = */ remoteType, /* aGroup */ nullptr, hal::ProcessPriority::PROCESS_PRIORITY_FOREGROUND, /* aPreferUsed */ true); } else { // We can find this event still in flight after having been asked to // shutdown. Let's fake a failure to ensure our callback is called // such that we clean up everything properly. onFinished = LaunchPromiseType::CreateAndReject( NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); } onFinished->Then(GetCurrentSerialEventTarget(), __func__, [callback = std::move(callback), remoteType](const CallbackParamType& aValue) mutable { callback(aValue, remoteType); }); }); SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()); } } // namespace dom } // namespace mozilla