diff options
Diffstat (limited to 'dom/workers/remoteworkers')
23 files changed, 4677 insertions, 0 deletions
diff --git a/dom/workers/remoteworkers/PRemoteWorker.ipdl b/dom/workers/remoteworkers/PRemoteWorker.ipdl new file mode 100644 index 0000000000..4da11c4840 --- /dev/null +++ b/dom/workers/remoteworkers/PRemoteWorker.ipdl @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; +include protocol PFetchEventOpProxy; + +include DOMTypes; +include ServiceWorkerOpArgs; +include RemoteWorkerTypes; + +namespace mozilla { +namespace dom { + +struct RemoteWorkerSuspendOp +{}; + +struct RemoteWorkerResumeOp +{}; + +struct RemoteWorkerFreezeOp +{}; + +struct RemoteWorkerThawOp +{}; + +struct RemoteWorkerTerminateOp +{}; + +struct RemoteWorkerPortIdentifierOp +{ + MessagePortIdentifier portIdentifier; +}; + +struct RemoteWorkerAddWindowIDOp +{ + uint64_t windowID; +}; + +struct RemoteWorkerRemoveWindowIDOp +{ + uint64_t windowID; +}; + +union RemoteWorkerOp { + RemoteWorkerSuspendOp; + RemoteWorkerResumeOp; + RemoteWorkerFreezeOp; + RemoteWorkerThawOp; + RemoteWorkerTerminateOp; + RemoteWorkerPortIdentifierOp; + RemoteWorkerAddWindowIDOp; + RemoteWorkerRemoveWindowIDOp; +}; + +// This protocol is used to make a remote worker controllable from the parent +// process. The parent process will receive operations from the +// PRemoteWorkerController protocol. +protocol PRemoteWorker +{ + manager PBackground; + + manages PFetchEventOpProxy; + +parent: + async Created(bool aStatus); + + async Error(ErrorValue aValue); + + async NotifyLock(bool aCreated); + + async NotifyWebTransport(bool aCreated); + + async Close(); + + async SetServiceWorkerSkipWaitingFlag() returns (bool aOk); + +child: + async PFetchEventOpProxy(ParentToChildServiceWorkerFetchEventOpArgs aArgs); + + async __delete__(); + + async ExecOp(RemoteWorkerOp op); + + async ExecServiceWorkerOp(ServiceWorkerOpArgs aArgs) + returns (ServiceWorkerOpResult aResult); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/PRemoteWorkerController.ipdl b/dom/workers/remoteworkers/PRemoteWorkerController.ipdl new file mode 100644 index 0000000000..059a81f8fe --- /dev/null +++ b/dom/workers/remoteworkers/PRemoteWorkerController.ipdl @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; +include protocol PFetchEventOp; + +include RemoteWorkerTypes; +include ServiceWorkerOpArgs; + +namespace mozilla { +namespace dom { + +/** + * Proxy protocol used by ServiceWorkerManager whose canonical state exists on + * the main thread to control/receive feedback from RemoteWorkers which are + * canonically controlled from the PBackground thread. Exclusively for use from + * the parent process main thread to the parent process PBackground thread. + */ +protocol PRemoteWorkerController { + manager PBackground; + + manages PFetchEventOp; + + child: + async CreationFailed(); + + async CreationSucceeded(); + + async ErrorReceived(ErrorValue aError); + + async Terminated(); + + async SetServiceWorkerSkipWaitingFlag() returns (bool aOk); + + parent: + async PFetchEventOp(ParentToParentServiceWorkerFetchEventOpArgs aArgs); + + async __delete__(); + + async Shutdown() returns (bool aOk); + + async ExecServiceWorkerOp(ServiceWorkerOpArgs aArgs) + returns (ServiceWorkerOpResult aResult); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/PRemoteWorkerService.ipdl b/dom/workers/remoteworkers/PRemoteWorkerService.ipdl new file mode 100644 index 0000000000..6287bb56a1 --- /dev/null +++ b/dom/workers/remoteworkers/PRemoteWorkerService.ipdl @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include ProtocolTypes; +include RemoteWorkerTypes; + +namespace mozilla { +namespace dom { + +// Simple protocol to register any active RemoteWorkerService running on any +// process. Initialization/registration is delayed for preallocated processes +// until the process takes on its final remoteType. +protocol PRemoteWorkerService +{ + manager PBackground; + +parent: + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerChild.cpp b/dom/workers/remoteworkers/RemoteWorkerChild.cpp new file mode 100644 index 0000000000..a644439a8d --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerChild.cpp @@ -0,0 +1,1071 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerChild.h" + +#include <utility> + +#include "MainThreadUtils.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIConsoleReportCollector.h" +#include "nsIInterfaceRequestor.h" +#include "nsIPrincipal.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +#include "RemoteWorkerService.h" +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/FetchEventOpProxyChild.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/RemoteWorkerManager.h" // RemoteWorkerManager::IsRemoteTypeAllowed +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/dom/ServiceWorkerInterceptController.h" +#include "mozilla/dom/ServiceWorkerOp.h" +#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h" +#include "mozilla/dom/ServiceWorkerShutdownState.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/workerinternals/ScriptLoader.h" +#include "mozilla/dom/WorkerError.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/PermissionManager.h" + +mozilla::LazyLogModule gRemoteWorkerChildLog("RemoteWorkerChild"); + +#ifdef LOG +# undef LOG +#endif +#define LOG(fmt) MOZ_LOG(gRemoteWorkerChildLog, mozilla::LogLevel::Verbose, fmt) + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +using workerinternals::ChannelFromScriptURLMainThread; + +namespace { + +class SharedWorkerInterfaceRequestor final : public nsIInterfaceRequestor { + public: + NS_DECL_ISUPPORTS + + SharedWorkerInterfaceRequestor() { + // This check must match the code nsDocShell::Create. + if (XRE_IsParentProcess()) { + mSWController = new ServiceWorkerInterceptController(); + } + } + + NS_IMETHOD + GetInterface(const nsIID& aIID, void** aSink) override { + MOZ_ASSERT(NS_IsMainThread()); + + if (mSWController && + aIID.Equals(NS_GET_IID(nsINetworkInterceptController))) { + // If asked for the network intercept controller, ask the outer requestor, + // which could be the docshell. + RefPtr<ServiceWorkerInterceptController> swController = mSWController; + swController.forget(aSink); + return NS_OK; + } + + return NS_NOINTERFACE; + } + + private: + ~SharedWorkerInterfaceRequestor() = default; + + RefPtr<ServiceWorkerInterceptController> mSWController; +}; + +NS_IMPL_ADDREF(SharedWorkerInterfaceRequestor) +NS_IMPL_RELEASE(SharedWorkerInterfaceRequestor) +NS_IMPL_QUERY_INTERFACE(SharedWorkerInterfaceRequestor, nsIInterfaceRequestor) + +// Normal runnable because AddPortIdentifier() is going to exec JS code. +class MessagePortIdentifierRunnable final : public WorkerRunnable { + public: + MessagePortIdentifierRunnable(WorkerPrivate* aWorkerPrivate, + RemoteWorkerChild* aActor, + const MessagePortIdentifier& aPortIdentifier) + : WorkerRunnable(aWorkerPrivate, "MessagePortIdentifierRunnable"), + mActor(aActor), + mPortIdentifier(aPortIdentifier) {} + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + if (aWorkerPrivate->GlobalScope()->IsDying()) { + mPortIdentifier.ForceClose(); + return true; + } + mActor->AddPortIdentifier(aCx, aWorkerPrivate, mPortIdentifier); + return true; + } + + RefPtr<RemoteWorkerChild> mActor; + UniqueMessagePortId mPortIdentifier; +}; + +// This is used to propagate the CSP violation when loading the SharedWorker +// main-script and nothing else. +class RemoteWorkerCSPEventListener final : public nsICSPEventListener { + public: + NS_DECL_ISUPPORTS + + explicit RemoteWorkerCSPEventListener(RemoteWorkerChild* aActor) + : mActor(aActor){}; + + NS_IMETHOD OnCSPViolationEvent(const nsAString& aJSON) override { + mActor->CSPViolationPropagationOnMainThread(aJSON); + return NS_OK; + } + + private: + ~RemoteWorkerCSPEventListener() = default; + + RefPtr<RemoteWorkerChild> mActor; +}; + +NS_IMPL_ISUPPORTS(RemoteWorkerCSPEventListener, nsICSPEventListener) + +} // anonymous namespace + +RemoteWorkerChild::RemoteWorkerChild(const RemoteWorkerData& aData) + : mState(VariantType<Pending>(), "RemoteWorkerChild::mState"), + mServiceKeepAlive(RemoteWorkerService::MaybeGetKeepAlive()), + mIsServiceWorker(aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData) { + MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread()); +} + +RemoteWorkerChild::~RemoteWorkerChild() { +#ifdef DEBUG + auto lock = mState.Lock(); + MOZ_ASSERT(lock->is<Killed>()); +#endif +} + +void RemoteWorkerChild::ActorDestroy(ActorDestroyReason) { + auto launcherData = mLauncherData.Access(); + + Unused << NS_WARN_IF(!launcherData->mTerminationPromise.IsEmpty()); + launcherData->mTerminationPromise.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, + __func__); + + auto lock = mState.Lock(); + + // If the worker hasn't shutdown or begun shutdown, we need to ensure it gets + // canceled. + if (NS_WARN_IF(!lock->is<Killed>() && !lock->is<Canceled>())) { + // In terms of strong references to this RemoteWorkerChild, at this moment: + // - IPC is holding a strong reference that will be dropped in the near + // future after this method returns. + // - If the worker has been started by ExecWorkerOnMainThread, then + // WorkerPrivate::mRemoteWorkerController is a strong reference to us. + // If the worker has not been started, ExecWorker's runnable lambda will + // have a strong reference that will cover the call to + // ExecWorkerOnMainThread. + // - The WorkerPrivate cancellation and termination callbacks will also + // hold strong references, but those callbacks will not outlive the + // WorkerPrivate and are not exposed to callers like + // mRemoteWorkerController is. + // + // Note that this call to RequestWorkerCancellation can still race worker + // cancellation, in which case the strong reference obtained by + // NewRunnableMethod can end up being the last strong reference. + // (RequestWorkerCancellation handles the case that the Worker is already + // canceled if this happens.) + RefPtr<nsIRunnable> runnable = + NewRunnableMethod("RequestWorkerCancellation", this, + &RemoteWorkerChild::RequestWorkerCancellation); + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(runnable.forget())); + } +} + +void RemoteWorkerChild::ExecWorker(const RemoteWorkerData& aData) { +#ifdef DEBUG + MOZ_ASSERT(GetActorEventTarget()->IsOnCurrentThread()); + auto launcherData = mLauncherData.Access(); + MOZ_ASSERT(CanSend()); +#endif + + RefPtr<RemoteWorkerChild> self = this; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), data = aData]() mutable { + nsresult rv = self->ExecWorkerOnMainThread(std::move(data)); + + // Creation failure will already have been reported via the method + // above internally using ScopeExit. + Unused << NS_WARN_IF(NS_FAILED(rv)); + }); + + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); +} + +nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) { + MOZ_ASSERT(NS_IsMainThread()); + + // Ensure that the IndexedDatabaseManager is initialized so that if any + // workers do any IndexedDB calls that all of IDB's prefs/etc. are + // initialized. + Unused << NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate()); + + auto scopeExit = + MakeScopeExit([&] { ExceptionalErrorTransitionDuringExecWorker(); }); + + // Verify the the RemoteWorker should be really allowed to run in this + // process, and fail if it shouldn't (This shouldn't normally happen, + // unless the RemoteWorkerData has been tempered in the process it was + // sent from). + if (!RemoteWorkerManager::IsRemoteTypeAllowed(aData)) { + return NS_ERROR_UNEXPECTED; + } + + auto principalOrErr = PrincipalInfoToPrincipal(aData.principalInfo()); + if (NS_WARN_IF(principalOrErr.isErr())) { + return principalOrErr.unwrapErr(); + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + auto loadingPrincipalOrErr = + PrincipalInfoToPrincipal(aData.loadingPrincipalInfo()); + if (NS_WARN_IF(loadingPrincipalOrErr.isErr())) { + return loadingPrincipalOrErr.unwrapErr(); + } + + auto partitionedPrincipalOrErr = + PrincipalInfoToPrincipal(aData.partitionedPrincipalInfo()); + if (NS_WARN_IF(partitionedPrincipalOrErr.isErr())) { + return partitionedPrincipalOrErr.unwrapErr(); + } + + WorkerLoadInfo info; + info.mBaseURI = DeserializeURI(aData.baseScriptURL()); + info.mResolvedScriptURI = DeserializeURI(aData.resolvedScriptURL()); + + info.mPrincipalInfo = MakeUnique<PrincipalInfo>(aData.principalInfo()); + info.mPartitionedPrincipalInfo = + MakeUnique<PrincipalInfo>(aData.partitionedPrincipalInfo()); + + info.mReferrerInfo = aData.referrerInfo(); + info.mDomain = aData.domain(); + info.mTrials = aData.originTrials(); + info.mPrincipal = principal; + info.mPartitionedPrincipal = partitionedPrincipalOrErr.unwrap(); + info.mLoadingPrincipal = loadingPrincipalOrErr.unwrap(); + info.mStorageAccess = aData.storageAccess(); + info.mUseRegularPrincipal = aData.useRegularPrincipal(); + info.mUsingStorageAccess = aData.usingStorageAccess(); + info.mIsThirdPartyContextToTopWindow = aData.isThirdPartyContextToTopWindow(); + info.mOriginAttributes = + BasePrincipal::Cast(principal)->OriginAttributesRef(); + info.mShouldResistFingerprinting = aData.shouldResistFingerprinting(); + Maybe<RFPTarget> overriddenFingerprintingSettings; + if (aData.overriddenFingerprintingSettings().isSome()) { + overriddenFingerprintingSettings.emplace( + RFPTarget(aData.overriddenFingerprintingSettings().ref())); + } + info.mOverriddenFingerprintingSettings = overriddenFingerprintingSettings; + net::CookieJarSettings::Deserialize(aData.cookieJarSettings(), + getter_AddRefs(info.mCookieJarSettings)); + info.mCookieJarSettingsArgs = aData.cookieJarSettings(); + + // Default CSP permissions for now. These will be overrided if necessary + // based on the script CSP headers during load in ScriptLoader. + info.mEvalAllowed = true; + info.mReportEvalCSPViolations = false; + info.mWasmEvalAllowed = true; + info.mReportWasmEvalCSPViolations = false; + info.mSecureContext = aData.isSecureContext() + ? WorkerLoadInfo::eSecureContext + : WorkerLoadInfo::eInsecureContext; + + WorkerPrivate::OverrideLoadInfoLoadGroup(info, info.mLoadingPrincipal); + + RefPtr<SharedWorkerInterfaceRequestor> requestor = + new SharedWorkerInterfaceRequestor(); + info.mInterfaceRequestor->SetOuterRequestor(requestor); + + Maybe<ClientInfo> clientInfo; + if (aData.clientInfo().isSome()) { + clientInfo.emplace(ClientInfo(aData.clientInfo().ref())); + } + + nsresult rv = NS_OK; + + if (clientInfo.isSome()) { + Maybe<mozilla::ipc::CSPInfo> cspInfo = clientInfo.ref().GetCspInfo(); + if (cspInfo.isSome()) { + info.mCSP = CSPInfoToCSP(cspInfo.ref(), nullptr); + info.mCSPInfo = MakeUnique<CSPInfo>(); + rv = CSPToCSPInfo(info.mCSP, info.mCSPInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + rv = info.SetPrincipalsAndCSPOnMainThread( + info.mPrincipal, info.mPartitionedPrincipal, info.mLoadGroup, info.mCSP); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString workerPrivateId; + + if (mIsServiceWorker) { + ServiceWorkerData& data = aData.serviceWorkerData().get_ServiceWorkerData(); + + MOZ_ASSERT(!data.id().IsEmpty()); + workerPrivateId = std::move(data.id()); + + info.mServiceWorkerCacheName = data.cacheName(); + info.mServiceWorkerDescriptor.emplace(data.descriptor()); + info.mServiceWorkerRegistrationDescriptor.emplace( + data.registrationDescriptor()); + info.mLoadFlags = static_cast<nsLoadFlags>(data.loadFlags()); + } else { + // Top level workers' main script use the document charset for the script + // uri encoding. + rv = ChannelFromScriptURLMainThread( + info.mLoadingPrincipal, nullptr /* parent document */, info.mLoadGroup, + info.mResolvedScriptURI, aData.type(), aData.credentials(), clientInfo, + nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER, info.mCookieJarSettings, + info.mReferrerInfo, getter_AddRefs(info.mChannel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsILoadInfo> loadInfo = info.mChannel->LoadInfo(); + + auto* cspEventListener = new RemoteWorkerCSPEventListener(this); + rv = loadInfo->SetCspEventListener(cspEventListener); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + info.mAgentClusterId = aData.agentClusterId(); + + AutoJSAPI jsapi; + jsapi.Init(); + + ErrorResult error; + RefPtr<RemoteWorkerChild> self = this; + RefPtr<WorkerPrivate> workerPrivate = WorkerPrivate::Constructor( + jsapi.cx(), aData.originalScriptURL(), false, + mIsServiceWorker ? WorkerKindService : WorkerKindShared, + aData.credentials(), aData.type(), aData.name(), VoidCString(), &info, + error, std::move(workerPrivateId), + [self](bool aEverRan) { + self->OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled(); + }, + // This will be invoked here on the main thread when the worker is already + // fully shutdown. This replaces a prior approach where we would + // begin to transition when the worker thread would reach the Canceling + // state. This lambda ensures that we not only wait for the Killing state + // to be reached but that the global shutdown has already occurred. + [self]() { self->TransitionStateFromCanceledToKilled(); }); + + if (NS_WARN_IF(error.Failed())) { + MOZ_ASSERT(!workerPrivate); + + rv = error.StealNSResult(); + return rv; + } + + workerPrivate->SetRemoteWorkerController(this); + + // This wants to run as a normal task sequentially after the top level script + // evaluation, so the hybrid target is the correct choice between hybrid and + // `ControlEventTarget`. + nsCOMPtr<nsISerialEventTarget> workerTarget = + workerPrivate->HybridEventTarget(); + + nsCOMPtr<nsIRunnable> runnable = NewCancelableRunnableMethod( + "InitialzeOnWorker", this, &RemoteWorkerChild::InitializeOnWorker); + + { + MOZ_ASSERT(workerPrivate); + auto lock = mState.Lock(); + // We MUST be pending here, so direct access is ok. + lock->as<Pending>().mWorkerPrivate = std::move(workerPrivate); + } + + if (mIsServiceWorker) { + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [workerTarget, + initializeWorkerRunnable = std::move(runnable)]() mutable { + Unused << NS_WARN_IF(NS_FAILED( + workerTarget->Dispatch(initializeWorkerRunnable.forget()))); + }); + + RefPtr<PermissionManager> permissionManager = + PermissionManager::GetInstance(); + if (!permissionManager) { + return NS_ERROR_FAILURE; + } + permissionManager->WhenPermissionsAvailable(principal, r); + } else { + if (NS_WARN_IF(NS_FAILED(workerTarget->Dispatch(runnable.forget())))) { + rv = NS_ERROR_FAILURE; + return rv; + } + } + + scopeExit.release(); + + return NS_OK; +} + +void RemoteWorkerChild::RequestWorkerCancellation() { + MOZ_ASSERT(NS_IsMainThread()); + + LOG(("RequestWorkerCancellation[this=%p]", this)); + + // We want to ensure that we've requested the worker be canceled. So if the + // worker is running, cancel it. We can't do this with the lock held, + // however, because our lambdas will want to manipulate the state. + RefPtr<WorkerPrivate> cancelWith; + { + auto lock = mState.Lock(); + if (lock->is<Pending>()) { + cancelWith = lock->as<Pending>().mWorkerPrivate; + } else if (lock->is<Running>()) { + cancelWith = lock->as<Running>().mWorkerPrivate; + } + } + + if (cancelWith) { + cancelWith->Cancel(); + } +} + +// This method will be invoked on the worker after the top-level +// CompileScriptRunnable task has succeeded and as long as the worker has not +// been closed/canceled. There are edge-cases related to cancellation, but we +// have our caller ensure that we are only called as long as the worker's state +// is Running. +// +// (https://bugzilla.mozilla.org/show_bug.cgi?id=1800659 will eliminate +// cancellation, and the documentation around that bug / in design documents +// helps provide more context about this.) +void RemoteWorkerChild::InitializeOnWorker() { + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod("TransitionStateToRunning", this, + &RemoteWorkerChild::TransitionStateToRunning); + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); +} + +RefPtr<GenericNonExclusivePromise> RemoteWorkerChild::GetTerminationPromise() { + auto launcherData = mLauncherData.Access(); + return launcherData->mTerminationPromise.Ensure(__func__); +} + +void RemoteWorkerChild::CreationSucceededOnAnyThread() { + CreationSucceededOrFailedOnAnyThread(true); +} + +void RemoteWorkerChild::CreationFailedOnAnyThread() { + CreationSucceededOrFailedOnAnyThread(false); +} + +void RemoteWorkerChild::CreationSucceededOrFailedOnAnyThread( + bool aDidCreationSucceed) { +#ifdef DEBUG + { + auto lock = mState.Lock(); + MOZ_ASSERT_IF(aDidCreationSucceed, lock->is<Running>()); + MOZ_ASSERT_IF(!aDidCreationSucceed, lock->is<Killed>()); + } +#endif + + RefPtr<RemoteWorkerChild> self = this; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, + [self = std::move(self), didCreationSucceed = aDidCreationSucceed] { + auto launcherData = self->mLauncherData.Access(); + + if (!self->CanSend() || launcherData->mDidSendCreated) { + return; + } + + Unused << self->SendCreated(didCreationSucceed); + launcherData->mDidSendCreated = true; + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::CloseWorkerOnMainThread() { + AssertIsOnMainThread(); + + LOG(("CloseWorkerOnMainThread[this=%p]", this)); + + // We can't hold the state lock while calling WorkerPrivate::Cancel because + // the lambda callback will want to touch the state, so save off the + // WorkerPrivate so we can cancel it (if we need to cancel it). + RefPtr<WorkerPrivate> cancelWith; + { + auto lock = mState.Lock(); + + if (lock->is<Pending>()) { + cancelWith = lock->as<Pending>().mWorkerPrivate; + // There should be no way for this code to run before we + // ExecWorkerOnMainThread runs, which means that either it should have + // set a WorkerPrivate on Pending, or its error handling should already + // have transitioned us to Canceled and Killing in that order. (It's + // also possible that it assigned a WorkerPrivate and subsequently we + // transitioned to Running, which would put us in the next branch.) + MOZ_DIAGNOSTIC_ASSERT(cancelWith); + } else if (lock->is<Running>()) { + cancelWith = lock->as<Running>().mWorkerPrivate; + } + } + + // It's very okay for us to not have a WorkerPrivate here if we've already + // canceled the worker or if errors happened. + if (cancelWith) { + cancelWith->Cancel(); + } +} + +/** + * Error reporting method + */ +void RemoteWorkerChild::ErrorPropagation(const ErrorValue& aValue) { + MOZ_ASSERT(GetActorEventTarget()->IsOnCurrentThread()); + + if (!CanSend()) { + return; + } + + Unused << SendError(aValue); +} + +void RemoteWorkerChild::ErrorPropagationDispatch(nsresult aError) { + MOZ_ASSERT(NS_FAILED(aError)); + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "RemoteWorkerChild::ErrorPropagationDispatch", + [self = std::move(self), aError]() { self->ErrorPropagation(aError); }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::ErrorPropagationOnMainThread( + const WorkerErrorReport* aReport, bool aIsErrorEvent) { + AssertIsOnMainThread(); + + ErrorValue value; + if (aIsErrorEvent) { + ErrorData data( + aReport->mIsWarning, aReport->mLineNumber, aReport->mColumnNumber, + aReport->mMessage, aReport->mFilename, aReport->mLine, + TransformIntoNewArray(aReport->mNotes, [](const WorkerErrorNote& note) { + return ErrorDataNote(note.mLineNumber, note.mColumnNumber, + note.mMessage, note.mFilename); + })); + value = data; + } else { + value = void_t(); + } + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "RemoteWorkerChild::ErrorPropagationOnMainThread", + [self = std::move(self), value]() { self->ErrorPropagation(value); }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::CSPViolationPropagationOnMainThread( + const nsAString& aJSON) { + AssertIsOnMainThread(); + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "RemoteWorkerChild::ErrorPropagationDispatch", + [self = std::move(self), json = nsString(aJSON)]() { + CSPViolation violation(json); + self->ErrorPropagation(violation); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::NotifyLock(bool aCreated) { + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [self = RefPtr(this), aCreated] { + if (!self->CanSend()) { + return; + } + + Unused << self->SendNotifyLock(aCreated); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::NotifyWebTransport(bool aCreated) { + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [self = RefPtr(this), aCreated] { + if (!self->CanSend()) { + return; + } + + Unused << self->SendNotifyWebTransport(aCreated); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::FlushReportsOnMainThread( + nsIConsoleReportCollector* aReporter) { + AssertIsOnMainThread(); + + bool reportErrorToBrowserConsole = true; + + // Flush the reports. + for (uint32_t i = 0, len = mWindowIDs.Length(); i < len; ++i) { + aReporter->FlushReportsToConsole( + mWindowIDs[i], nsIConsoleReportCollector::ReportAction::Save); + reportErrorToBrowserConsole = false; + } + + // Finally report to browser console if there is no any window. + if (reportErrorToBrowserConsole) { + aReporter->FlushReportsToConsole(0); + return; + } + + aReporter->ClearConsoleReports(); +} + +/** + * Worker state transition methods + */ +RemoteWorkerChild::WorkerPrivateAccessibleState:: + ~WorkerPrivateAccessibleState() { + // We should now only be performing state transitions on the main thread, so + // we should assert we're only releasing on the main thread. + MOZ_ASSERT(!mWorkerPrivate || NS_IsMainThread()); + // mWorkerPrivate can be safely released on the main thread. + if (!mWorkerPrivate || NS_IsMainThread()) { + return; + } + + // But as a backstop, do proxy the release to the main thread. + NS_ReleaseOnMainThread( + "RemoteWorkerChild::WorkerPrivateAccessibleState::mWorkerPrivate", + mWorkerPrivate.forget()); +} + +void RemoteWorkerChild:: + OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled() { + auto lock = mState.Lock(); + + LOG(("TransitionStateFromPendingOrRunningToCanceled[this=%p]", this)); + + if (lock->is<Pending>()) { + TransitionStateFromPendingToCanceled(lock.ref()); + } else if (lock->is<Running>()) { + *lock = VariantType<Canceled>(); + } else { + MOZ_ASSERT(false, "State should have been Pending or Running"); + } +} + +void RemoteWorkerChild::TransitionStateFromPendingToCanceled(State& aState) { + AssertIsOnMainThread(); + MOZ_ASSERT(aState.is<Pending>()); + LOG(("TransitionStateFromPendingToCanceled[this=%p]", this)); + + CancelAllPendingOps(aState); + + aState = VariantType<Canceled>(); +} + +void RemoteWorkerChild::TransitionStateFromCanceledToKilled() { + AssertIsOnMainThread(); + + LOG(("TransitionStateFromCanceledToKilled[this=%p]", this)); + + auto lock = mState.Lock(); + MOZ_ASSERT(lock->is<Canceled>()); + + *lock = VariantType<Killed>(); + + RefPtr<RemoteWorkerChild> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self]() { + auto launcherData = self->mLauncherData.Access(); + + // (We maintain the historical ordering of resolving this promise prior to + // calling SendClose, however the previous code used 2 separate dispatches + // to this thread for the resolve and SendClose, and there inherently + // would be a race between the runnables resulting from the resolved + // promise and the promise containing the call to SendClose. Now it's + // entirely clear that our call to SendClose will effectively run before + // any of the resolved promises are able to do anything.) + launcherData->mTerminationPromise.ResolveIfExists(true, __func__); + + if (self->CanSend()) { + Unused << self->SendClose(); + } + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void RemoteWorkerChild::TransitionStateToRunning() { + AssertIsOnMainThread(); + + LOG(("TransitionStateToRunning[this=%p]", this)); + + nsTArray<RefPtr<Op>> pendingOps; + + { + auto lock = mState.Lock(); + + // Because this is an async notification sent from the worker to the main + // thread, it's very possible that we've already decided on the main thread + // to transition to the Canceled state, in which case there is nothing for + // us to do here. + if (!lock->is<Pending>()) { + LOG(("State is already not pending in TransitionStateToRunning[this=%p]!", + this)); + return; + } + + RefPtr<WorkerPrivate> workerPrivate = + std::move(lock->as<Pending>().mWorkerPrivate); + pendingOps = std::move(lock->as<Pending>().mPendingOps); + + // Move the worker private into place to avoid gratuitous ref churn; prior + // comments here suggest the Variant can't accept a move. + *lock = VariantType<Running>(); + lock->as<Running>().mWorkerPrivate = std::move(workerPrivate); + } + + CreationSucceededOnAnyThread(); + + RefPtr<RemoteWorkerChild> self = this; + for (auto& op : pendingOps) { + op->StartOnMainThread(self); + } +} + +void RemoteWorkerChild::ExceptionalErrorTransitionDuringExecWorker() { + AssertIsOnMainThread(); + + LOG(("ExceptionalErrorTransitionDuringExecWorker[this=%p]", this)); + + // This method is called synchronously by ExecWorkerOnMainThread in the event + // of any error. Because we only transition to Running on the main thread + // as the result of a notification from the worker, we know our state will be + // Pending, but mWorkerPrivate may or may not be null, as we may not have + // gotten to spawning the worker. + // + // In the event the worker exists, we need to Cancel() it. We must do this + // without the lock held because our call to Cancel() will invoke the + // cancellation callback we created which will call TransitionStateToCanceled, + // and we can't be holding the lock when that happens. + + RefPtr<WorkerPrivate> cancelWith; + + { + auto lock = mState.Lock(); + + MOZ_ASSERT(lock->is<Pending>()); + if (lock->is<Pending>()) { + cancelWith = lock->as<Pending>().mWorkerPrivate; + if (!cancelWith) { + // The worker wasn't actually created, so we should synthetically + // transition to canceled and onward. Since we have the lock, + // perform the transition now for clarity, but we'll handle the rest of + // this case after dropping the lock. + TransitionStateFromPendingToCanceled(lock.ref()); + } + } + } + + if (cancelWith) { + cancelWith->Cancel(); + } else { + TransitionStateFromCanceledToKilled(); + CreationFailedOnAnyThread(); + } +} + +/** + * Operation execution classes/methods + */ +class RemoteWorkerChild::SharedWorkerOp : public RemoteWorkerChild::Op { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedWorkerOp, override) + + explicit SharedWorkerOp(RemoteWorkerOp&& aOp) : mOp(std::move(aOp)) {} + + bool MaybeStart(RemoteWorkerChild* aOwner, + RemoteWorkerChild::State& aState) override { + MOZ_ASSERT(!mStarted); + MOZ_ASSERT(aOwner); + // Thread: We are on the Worker Launcher thread. + + // Return false, indicating we should queue this op if our current state is + // pending and this isn't a termination op (which should skip the line). + if (aState.is<Pending>() && !IsTerminationOp()) { + return false; + } + + // If the worker is already shutting down (which should be unexpected + // because we should be told new operations after a termination op), just + // return true to indicate the op should be discarded. + if (aState.is<Canceled>() || aState.is<Killed>()) { +#ifdef DEBUG + mStarted = true; +#endif + if (mOp.type() == RemoteWorkerOp::TRemoteWorkerPortIdentifierOp) { + MessagePort::ForceClose( + mOp.get_RemoteWorkerPortIdentifierOp().portIdentifier()); + } + return true; + } + + MOZ_ASSERT(aState.is<Running>() || IsTerminationOp()); + + RefPtr<SharedWorkerOp> self = this; + RefPtr<RemoteWorkerChild> owner = aOwner; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), owner = std::move(owner)]() mutable { + { + auto lock = owner->mState.Lock(); + + if (NS_WARN_IF(lock->is<Canceled>() || lock->is<Killed>())) { + self->Cancel(); + // Worker has already canceled, force close the MessagePort. + if (self->mOp.type() == + RemoteWorkerOp::TRemoteWorkerPortIdentifierOp) { + MessagePort::ForceClose( + self->mOp.get_RemoteWorkerPortIdentifierOp() + .portIdentifier()); + } + return; + } + } + + self->StartOnMainThread(owner); + }); + + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); + +#ifdef DEBUG + mStarted = true; +#endif + + return true; + } + + void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) final { + using Running = RemoteWorkerChild::Running; + + AssertIsOnMainThread(); + + if (IsTerminationOp()) { + aOwner->CloseWorkerOnMainThread(); + return; + } + + auto lock = aOwner->mState.Lock(); + MOZ_ASSERT(lock->is<Running>()); + if (!lock->is<Running>()) { + aOwner->ErrorPropagationDispatch(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<WorkerPrivate> workerPrivate = lock->as<Running>().mWorkerPrivate; + + MOZ_ASSERT(workerPrivate); + + if (mOp.type() == RemoteWorkerOp::TRemoteWorkerSuspendOp) { + workerPrivate->ParentWindowPaused(); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerResumeOp) { + workerPrivate->ParentWindowResumed(); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerFreezeOp) { + workerPrivate->Freeze(nullptr); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerThawOp) { + workerPrivate->Thaw(nullptr); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerPortIdentifierOp) { + RefPtr<MessagePortIdentifierRunnable> r = + new MessagePortIdentifierRunnable( + workerPrivate, aOwner, + mOp.get_RemoteWorkerPortIdentifierOp().portIdentifier()); + if (NS_WARN_IF(!r->Dispatch())) { + aOwner->ErrorPropagationDispatch(NS_ERROR_FAILURE); + } + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerAddWindowIDOp) { + aOwner->mWindowIDs.AppendElement( + mOp.get_RemoteWorkerAddWindowIDOp().windowID()); + } else if (mOp.type() == RemoteWorkerOp::TRemoteWorkerRemoveWindowIDOp) { + aOwner->mWindowIDs.RemoveElement( + mOp.get_RemoteWorkerRemoveWindowIDOp().windowID()); + } else { + MOZ_CRASH("Unknown RemoteWorkerOp type!"); + } + } + + void Cancel() override { +#ifdef DEBUG + mStarted = true; +#endif + } + + private: + ~SharedWorkerOp() { MOZ_ASSERT(mStarted); } + + bool IsTerminationOp() const { + return mOp.type() == RemoteWorkerOp::TRemoteWorkerTerminateOp; + } + + RemoteWorkerOp mOp; + +#ifdef DEBUG + bool mStarted = false; +#endif +}; + +void RemoteWorkerChild::AddPortIdentifier( + JSContext* aCx, WorkerPrivate* aWorkerPrivate, + UniqueMessagePortId& aPortIdentifier) { + if (NS_WARN_IF(!aWorkerPrivate->ConnectMessagePort(aCx, aPortIdentifier))) { + ErrorPropagationDispatch(NS_ERROR_FAILURE); + } +} + +void RemoteWorkerChild::CancelAllPendingOps(State& aState) { + MOZ_ASSERT(aState.is<Pending>()); + + auto pendingOps = std::move(aState.as<Pending>().mPendingOps); + + for (auto& op : pendingOps) { + op->Cancel(); + } +} + +void RemoteWorkerChild::MaybeStartOp(RefPtr<Op>&& aOp) { + MOZ_ASSERT(aOp); + + auto lock = mState.Lock(); + + if (!aOp->MaybeStart(this, lock.ref())) { + // Maybestart returns false only if we are <Pending>. + lock->as<Pending>().mPendingOps.AppendElement(std::move(aOp)); + } +} + +IPCResult RemoteWorkerChild::RecvExecOp(RemoteWorkerOp&& aOp) { + MOZ_ASSERT(!mIsServiceWorker); + + MaybeStartOp(new SharedWorkerOp(std::move(aOp))); + + return IPC_OK(); +} + +IPCResult RemoteWorkerChild::RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve) { + MOZ_ASSERT(mIsServiceWorker); + MOZ_ASSERT( + aArgs.type() != + ServiceWorkerOpArgs::TParentToChildServiceWorkerFetchEventOpArgs, + "FetchEvent operations should be sent via PFetchEventOp(Proxy) actors!"); + + MaybeReportServiceWorkerShutdownProgress(aArgs); + + MaybeStartOp(ServiceWorkerOp::Create(std::move(aArgs), std::move(aResolve))); + + return IPC_OK(); +} + +RefPtr<GenericPromise> +RemoteWorkerChild::MaybeSendSetServiceWorkerSkipWaitingFlag() { + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + RefPtr<RemoteWorkerChild> self = this; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self = std::move( + self), + promise] { + if (!self->CanSend()) { + promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + return; + } + + self->SendSetServiceWorkerSkipWaitingFlag()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise]( + const SetServiceWorkerSkipWaitingFlagPromise::ResolveOrRejectValue& + aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + return; + } + + promise->Resolve(aResult.ResolveValue(), __func__); + }); + }); + + GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + + return promise; +} + +/** + * PFetchEventOpProxy methods + */ +already_AddRefed<PFetchEventOpProxyChild> +RemoteWorkerChild::AllocPFetchEventOpProxyChild( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) { + return RefPtr{new FetchEventOpProxyChild()}.forget(); +} + +IPCResult RemoteWorkerChild::RecvPFetchEventOpProxyConstructor( + PFetchEventOpProxyChild* aActor, + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) { + MOZ_ASSERT(aActor); + + (static_cast<FetchEventOpProxyChild*>(aActor))->Initialize(aArgs); + + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerChild.h b/dom/workers/remoteworkers/RemoteWorkerChild.h new file mode 100644 index 0000000000..cdb2fb32d7 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerChild.h @@ -0,0 +1,239 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerChild_h +#define mozilla_dom_RemoteWorkerChild_h + +#include "nsCOMPtr.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +#include "mozilla/DataMutex.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ThreadBound.h" +#include "mozilla/dom/PRemoteWorkerChild.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" + +class nsISerialEventTarget; +class nsIConsoleReportCollector; + +namespace mozilla::dom { + +class ErrorValue; +class FetchEventOpProxyChild; +class RemoteWorkerData; +class RemoteWorkerServiceKeepAlive; +class ServiceWorkerOp; +class UniqueMessagePortId; +class WeakWorkerRef; +class WorkerErrorReport; +class WorkerPrivate; + +/** + * Background-managed "Worker Launcher"-thread-resident created via the + * RemoteWorkerManager to actually spawn the worker. Currently, the worker will + * be spawned from the main thread due to nsIPrincipal not being able to be + * created on background threads and other ownership invariants, most of which + * can be relaxed in the future. + */ +class RemoteWorkerChild final : public PRemoteWorkerChild { + friend class FetchEventOpProxyChild; + friend class PRemoteWorkerChild; + friend class ServiceWorkerOp; + + ~RemoteWorkerChild(); + + public: + // Note that all IPC-using methods must only be invoked on the + // RemoteWorkerService thread which the inherited + // IProtocol::GetActorEventTarget() will return for us. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteWorkerChild, final) + + explicit RemoteWorkerChild(const RemoteWorkerData& aData); + + void ExecWorker(const RemoteWorkerData& aData); + + void ErrorPropagationOnMainThread(const WorkerErrorReport* aReport, + bool aIsErrorEvent); + + void CSPViolationPropagationOnMainThread(const nsAString& aJSON); + + void NotifyLock(bool aCreated); + + void NotifyWebTransport(bool aCreated); + + void FlushReportsOnMainThread(nsIConsoleReportCollector* aReporter); + + void AddPortIdentifier(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + UniqueMessagePortId& aPortIdentifier); + + RefPtr<GenericNonExclusivePromise> GetTerminationPromise(); + + RefPtr<GenericPromise> MaybeSendSetServiceWorkerSkipWaitingFlag(); + + const nsTArray<uint64_t>& WindowIDs() const { return mWindowIDs; } + + private: + class InitializeWorkerRunnable; + + class Op; + class SharedWorkerOp; + + struct WorkerPrivateAccessibleState { + ~WorkerPrivateAccessibleState(); + RefPtr<WorkerPrivate> mWorkerPrivate; + }; + + // Initial state, mWorkerPrivate is initially null but will be initialized on + // the main thread by ExecWorkerOnMainThread when the WorkerPrivate is + // created. The state will transition to Running or Canceled, also from the + // main thread. + struct Pending : WorkerPrivateAccessibleState { + nsTArray<RefPtr<Op>> mPendingOps; + }; + + // Running, with the state transition happening on the main thread as a result + // of the worker successfully processing our initialization runnable, + // indicating that top-level script execution successfully completed. Because + // all of our state transitions happen on the main thread and are posed in + // terms of the main thread's perspective of the worker's state, it's very + // possible for us to skip directly from Pending to Canceled because we decide + // to cancel/terminate the worker prior to it finishing script loading or + // reporting back to us. + struct Running : WorkerPrivateAccessibleState {}; + + // Cancel() has been called on the WorkerPrivate on the main thread by a + // TerminationOp, top-level script evaluation has failed and canceled the + // worker, or in the case of a SharedWorker, close() has been called on + // the global scope by content code and the worker has advanced to the + // Canceling state. (Dedicated Workers can also self close, but they will + // never be RemoteWorkers. Although a SharedWorker can own DedicatedWorkers.) + // Browser shutdown will result in a TerminationOp thanks to use of a shutdown + // blocker in the parent, so the RuntimeService shouldn't get involved, but we + // would also handle that case acceptably too. + // + // Because worker self-closing is still handled by dispatching a runnable to + // the main thread to effectively call WorkerPrivate::Cancel(), there isn't + // a race between a worker deciding to self-close and our termination ops. + // + // In this state, we have dropped the reference to the WorkerPrivate and will + // no longer be dispatching runnables to the worker. We wait in this state + // until the termination lambda is invoked letting us know that the worker has + // entirely shutdown and we can advanced to the Killed state. + struct Canceled {}; + + // The worker termination lambda has been invoked and we know the Worker is + // entirely shutdown. (Inherently it is possible for us to advance to this + // state while the nsThread for the worker is still in the process of + // shutting down, but no more worker code will run on it.) + // + // This name is chosen to match the Worker's own state model. + struct Killed {}; + + using State = Variant<Pending, Running, Canceled, Killed>; + + // The state of the WorkerPrivate as perceived by the owner on the main + // thread. All state transitions now happen on the main thread, but the + // Worker Launcher thread will consult the state and will directly append ops + // to the Pending queue + DataMutex<State> mState; + + const RefPtr<RemoteWorkerServiceKeepAlive> mServiceKeepAlive; + + class Op { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual ~Op() = default; + + virtual bool MaybeStart(RemoteWorkerChild* aOwner, State& aState) = 0; + + virtual void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) = 0; + + virtual void Cancel() = 0; + }; + + void ActorDestroy(ActorDestroyReason) override; + + mozilla::ipc::IPCResult RecvExecOp(RemoteWorkerOp&& aOp); + + mozilla::ipc::IPCResult RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve); + + already_AddRefed<PFetchEventOpProxyChild> AllocPFetchEventOpProxyChild( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs); + + mozilla::ipc::IPCResult RecvPFetchEventOpProxyConstructor( + PFetchEventOpProxyChild* aActor, + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) override; + + nsresult ExecWorkerOnMainThread(RemoteWorkerData&& aData); + + void ExceptionalErrorTransitionDuringExecWorker(); + + void RequestWorkerCancellation(); + + void InitializeOnWorker(); + + void CreationSucceededOnAnyThread(); + + void CreationFailedOnAnyThread(); + + void CreationSucceededOrFailedOnAnyThread(bool aDidCreationSucceed); + + // Cancels the worker if it has been started and ensures that we transition + // to the Terminated state once the worker has been terminated or we have + // ensured that it will never start. + void CloseWorkerOnMainThread(); + + void ErrorPropagation(const ErrorValue& aValue); + + void ErrorPropagationDispatch(nsresult aError); + + // When the WorkerPrivate Cancellation lambda is invoked, it's possible that + // we have not yet advanced to running from pending, so we could be in either + // state. This method is expected to be called by the Workers' cancellation + // lambda and will obtain the lock and call the + // TransitionStateFromPendingToCanceled if appropriate. Otherwise it will + // directly move from the running state to the canceled state which does not + // require additional cleanup. + void OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled(); + // A helper used by the above method by the worker cancellation lambda if the + // the worker hasn't started running, or in exceptional cases where we bail + // out of the ExecWorker method early. The caller must be holding the lock + // (in order to pass in the state). + void TransitionStateFromPendingToCanceled(State& aState); + void TransitionStateFromCanceledToKilled(); + + void TransitionStateToRunning(); + + void TransitionStateToTerminated(); + + void TransitionStateToTerminated(State& aState); + + void CancelAllPendingOps(State& aState); + + void MaybeStartOp(RefPtr<Op>&& aOp); + + const bool mIsServiceWorker; + + // Touched on main-thread only. + nsTArray<uint64_t> mWindowIDs; + + struct LauncherBoundData { + MozPromiseHolder<GenericNonExclusivePromise> mTerminationPromise; + // Flag to ensure we report creation at most once. This could be cleaned up + // further. + bool mDidSendCreated = false; + }; + + ThreadBound<LauncherBoundData> mLauncherData; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerChild_h diff --git a/dom/workers/remoteworkers/RemoteWorkerController.cpp b/dom/workers/remoteworkers/RemoteWorkerController.cpp new file mode 100644 index 0000000000..b0d56aa33d --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerController.cpp @@ -0,0 +1,573 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerController.h" + +#include <utility> + +#include "nsDebug.h" + +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Maybe.h" +#include "mozilla/RemoteLazyInputStreamStorage.h" +#include "mozilla/dom/FetchEventOpParent.h" +#include "mozilla/dom/FetchEventOpProxyParent.h" +#include "mozilla/dom/MessagePortParent.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/ServiceWorkerCloneData.h" +#include "mozilla/dom/ServiceWorkerShutdownState.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "RemoteWorkerControllerParent.h" +#include "RemoteWorkerManager.h" +#include "RemoteWorkerParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +/* static */ +already_AddRefed<RemoteWorkerController> RemoteWorkerController::Create( + const RemoteWorkerData& aData, RemoteWorkerObserver* aObserver, + base::ProcessId aProcessId) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aObserver); + + RefPtr<RemoteWorkerController> controller = + new RemoteWorkerController(aData, aObserver); + + RefPtr<RemoteWorkerManager> manager = RemoteWorkerManager::GetOrCreate(); + MOZ_ASSERT(manager); + + // XXX: We do not check for failure here, should we? + manager->Launch(controller, aData, aProcessId); + + return controller.forget(); +} + +RemoteWorkerController::RemoteWorkerController(const RemoteWorkerData& aData, + RemoteWorkerObserver* aObserver) + : mObserver(aObserver), + mState(ePending), + mIsServiceWorker(aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); +} + +RemoteWorkerController::~RemoteWorkerController() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mPendingOps.IsEmpty()); +} + +void RemoteWorkerController::SetWorkerActor(RemoteWorkerParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActor); + MOZ_ASSERT(aActor); + + mActor = aActor; +} + +void RemoteWorkerController::NoteDeadWorkerActor() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActor); + + // The actor has been destroyed without a proper close() notification. Let's + // inform the observer. + if (mState == eReady) { + mObserver->Terminated(); + } + + mActor = nullptr; + + Shutdown(); +} + +void RemoteWorkerController::CreationFailed() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(mState == ePending || mState == eTerminated); + + if (mState == eTerminated) { + MOZ_ASSERT(!mActor); + MOZ_ASSERT(mPendingOps.IsEmpty()); + // Nothing to do. + return; + } + + NoteDeadWorker(); + + mObserver->CreationFailed(); +} + +void RemoteWorkerController::CreationSucceeded() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == ePending || mState == eTerminated); + + if (mState == eTerminated) { + MOZ_ASSERT(!mActor); + MOZ_ASSERT(mPendingOps.IsEmpty()); + // Nothing to do. + return; + } + + MOZ_ASSERT(mActor); + mState = eReady; + + mObserver->CreationSucceeded(); + + auto pendingOps = std::move(mPendingOps); + + for (auto& op : pendingOps) { + DebugOnly<bool> started = op->MaybeStart(this); + MOZ_ASSERT(started); + } +} + +void RemoteWorkerController::ErrorPropagation(const ErrorValue& aValue) { + AssertIsOnBackgroundThread(); + + mObserver->ErrorReceived(aValue); +} + +void RemoteWorkerController::NotifyLock(bool aCreated) { + AssertIsOnBackgroundThread(); + + mObserver->LockNotified(aCreated); +} + +void RemoteWorkerController::NotifyWebTransport(bool aCreated) { + AssertIsOnBackgroundThread(); + + mObserver->WebTransportNotified(aCreated); +} + +void RemoteWorkerController::WorkerTerminated() { + AssertIsOnBackgroundThread(); + + NoteDeadWorker(); + + mObserver->Terminated(); +} + +void RemoteWorkerController::CancelAllPendingOps() { + AssertIsOnBackgroundThread(); + + auto pendingOps = std::move(mPendingOps); + + for (auto& op : pendingOps) { + op->Cancel(); + } +} + +void RemoteWorkerController::Shutdown() { + AssertIsOnBackgroundThread(); + Unused << NS_WARN_IF(mIsServiceWorker && !mPendingOps.IsEmpty()); + + if (mState == eTerminated) { + MOZ_ASSERT(mPendingOps.IsEmpty()); + return; + } + + mState = eTerminated; + + CancelAllPendingOps(); + + if (!mActor) { + return; + } + + mActor->SetController(nullptr); + + /** + * The "non-remote-side" of the Service Worker will have ensured that the + * remote worker is terminated before calling `Shutdown().` + */ + if (mIsServiceWorker) { + mActor->MaybeSendDelete(); + } else { + Unused << mActor->SendExecOp(RemoteWorkerTerminateOp()); + } + + mActor = nullptr; +} + +void RemoteWorkerController::NoteDeadWorker() { + AssertIsOnBackgroundThread(); + + CancelAllPendingOps(); + + /** + * The "non-remote-side" of the Service Worker will initiate `Shutdown()` + * once it's notified that all dispatched operations have either completed + * or canceled. That is, it'll explicitly call `Shutdown()` later. + */ + if (!mIsServiceWorker) { + Shutdown(); + } +} + +template <typename... Args> +void RemoteWorkerController::MaybeStartSharedWorkerOp(Args&&... aArgs) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mIsServiceWorker); + + UniquePtr<PendingSharedWorkerOp> op = + MakeUnique<PendingSharedWorkerOp>(std::forward<Args>(aArgs)...); + + if (!op->MaybeStart(this)) { + mPendingOps.AppendElement(std::move(op)); + } +} + +void RemoteWorkerController::AddWindowID(uint64_t aWindowID) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aWindowID); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eAddWindowID, aWindowID); +} + +void RemoteWorkerController::RemoveWindowID(uint64_t aWindowID) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aWindowID); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eRemoveWindowID, aWindowID); +} + +void RemoteWorkerController::AddPortIdentifier( + const MessagePortIdentifier& aPortIdentifier) { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(aPortIdentifier); +} + +void RemoteWorkerController::Terminate() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eTerminate); +} + +void RemoteWorkerController::Suspend() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eSuspend); +} + +void RemoteWorkerController::Resume() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eResume); +} + +void RemoteWorkerController::Freeze() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eFreeze); +} + +void RemoteWorkerController::Thaw() { + AssertIsOnBackgroundThread(); + + MaybeStartSharedWorkerOp(PendingSharedWorkerOp::eThaw); +} + +RefPtr<ServiceWorkerOpPromise> RemoteWorkerController::ExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIsServiceWorker); + + RefPtr<ServiceWorkerOpPromise::Private> promise = + new ServiceWorkerOpPromise::Private(__func__); + + UniquePtr<PendingServiceWorkerOp> op = + MakeUnique<PendingServiceWorkerOp>(std::move(aArgs), promise); + + if (!op->MaybeStart(this)) { + mPendingOps.AppendElement(std::move(op)); + } + + return promise; +} + +RefPtr<ServiceWorkerFetchEventOpPromise> +RemoteWorkerController::ExecServiceWorkerFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<FetchEventOpParent> aReal) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIsServiceWorker); + + RefPtr<ServiceWorkerFetchEventOpPromise::Private> promise = + new ServiceWorkerFetchEventOpPromise::Private(__func__); + + UniquePtr<PendingSWFetchEventOp> op = + MakeUnique<PendingSWFetchEventOp>(aArgs, promise, std::move(aReal)); + + if (!op->MaybeStart(this)) { + mPendingOps.AppendElement(std::move(op)); + } + + return promise; +} + +RefPtr<GenericPromise> RemoteWorkerController::SetServiceWorkerSkipWaitingFlag() + const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mObserver); + + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + + static_cast<RemoteWorkerControllerParent*>(mObserver.get()) + ->MaybeSendSetServiceWorkerSkipWaitingFlag( + [promise](bool aOk) { promise->Resolve(aOk, __func__); }); + + return promise; +} + +bool RemoteWorkerController::IsTerminated() const { + return mState == eTerminated; +} + +RemoteWorkerController::PendingSharedWorkerOp::PendingSharedWorkerOp( + Type aType, uint64_t aWindowID) + : mType(aType), mWindowID(aWindowID) { + AssertIsOnBackgroundThread(); +} + +RemoteWorkerController::PendingSharedWorkerOp::PendingSharedWorkerOp( + const MessagePortIdentifier& aPortIdentifier) + : mType(ePortIdentifier), mPortIdentifier(aPortIdentifier) { + AssertIsOnBackgroundThread(); +} + +RemoteWorkerController::PendingSharedWorkerOp::~PendingSharedWorkerOp() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(mCompleted); +} + +bool RemoteWorkerController::PendingSharedWorkerOp::MaybeStart( + RemoteWorkerController* const aOwner) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mCompleted); + MOZ_ASSERT(aOwner); + + if (aOwner->mState == RemoteWorkerController::eTerminated) { + Cancel(); + return true; + } + + if (aOwner->mState == RemoteWorkerController::ePending && + mType != eTerminate) { + return false; + } + + switch (mType) { + case eTerminate: + aOwner->Shutdown(); + break; + case eSuspend: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerSuspendOp()); + break; + case eResume: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerResumeOp()); + break; + case eFreeze: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerFreezeOp()); + break; + case eThaw: + Unused << aOwner->mActor->SendExecOp(RemoteWorkerThawOp()); + break; + case ePortIdentifier: + Unused << aOwner->mActor->SendExecOp( + RemoteWorkerPortIdentifierOp(mPortIdentifier)); + break; + case eAddWindowID: + Unused << aOwner->mActor->SendExecOp( + RemoteWorkerAddWindowIDOp(mWindowID)); + break; + case eRemoveWindowID: + Unused << aOwner->mActor->SendExecOp( + RemoteWorkerRemoveWindowIDOp(mWindowID)); + break; + default: + MOZ_CRASH("Unknown op."); + } + + mCompleted = true; + + return true; +} + +void RemoteWorkerController::PendingSharedWorkerOp::Cancel() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mCompleted); + + // We don't want to leak the port if the operation has not been processed. + if (mType == ePortIdentifier) { + MessagePortParent::ForceClose(mPortIdentifier.uuid(), + mPortIdentifier.destinationUuid(), + mPortIdentifier.sequenceId()); + } + + mCompleted = true; +} + +RemoteWorkerController::PendingServiceWorkerOp::PendingServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, + RefPtr<ServiceWorkerOpPromise::Private> aPromise) + : mArgs(std::move(aArgs)), mPromise(std::move(aPromise)) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); +} + +RemoteWorkerController::PendingServiceWorkerOp::~PendingServiceWorkerOp() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(!mPromise); +} + +bool RemoteWorkerController::PendingServiceWorkerOp::MaybeStart( + RemoteWorkerController* const aOwner) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + MOZ_ASSERT(aOwner); + + if (NS_WARN_IF(aOwner->mState == RemoteWorkerController::eTerminated)) { + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; + return true; + } + + // The target content process must still be starting up. + if (!aOwner->mActor) { + // We can avoid starting the worker at all if we know it should be + // terminated. + MOZ_ASSERT(aOwner->mState == RemoteWorkerController::ePending); + if (mArgs.type() == + ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs) { + aOwner->CancelAllPendingOps(); + Cancel(); + + aOwner->mState = RemoteWorkerController::eTerminated; + + return true; + } + + return false; + } + + /** + * Allow termination operations to pass through while pending because the + * remote Service Worker can be terminated while still starting up. + */ + if (aOwner->mState == RemoteWorkerController::ePending && + mArgs.type() != + ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs) { + return false; + } + + MaybeReportServiceWorkerShutdownProgress(mArgs); + + aOwner->mActor->SendExecServiceWorkerOp(mArgs)->Then( + GetCurrentSerialEventTarget(), __func__, + [promise = std::move(mPromise)]( + PRemoteWorkerParent::ExecServiceWorkerOpPromise:: + ResolveOrRejectValue&& aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + return; + } + + promise->Resolve(std::move(aResult.ResolveValue()), __func__); + }); + + return true; +} + +void RemoteWorkerController::PendingServiceWorkerOp::Cancel() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; +} + +RemoteWorkerController::PendingSWFetchEventOp::PendingSWFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<ServiceWorkerFetchEventOpPromise::Private> aPromise, + RefPtr<FetchEventOpParent>&& aReal) + : mArgs(aArgs), mPromise(std::move(aPromise)), mReal(aReal) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + + // If there is a TParentToParentStream in the request body, we need to + // save it to our stream. + IPCInternalRequest& req = mArgs.common().internalRequest(); + if (req.body().isSome() && + req.body().ref().type() == BodyStreamVariant::TParentToParentStream) { + nsCOMPtr<nsIInputStream> stream; + auto streamLength = req.bodySize(); + const auto& uuid = req.body().ref().get_ParentToParentStream().uuid(); + + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + MOZ_DIAGNOSTIC_ASSERT(storage); + storage->GetStream(uuid, 0, streamLength, getter_AddRefs(mBodyStream)); + storage->ForgetStream(uuid); + + MOZ_DIAGNOSTIC_ASSERT(mBodyStream); + + req.body() = Nothing(); + } +} + +RemoteWorkerController::PendingSWFetchEventOp::~PendingSWFetchEventOp() { + AssertIsOnBackgroundThread(); + MOZ_DIAGNOSTIC_ASSERT(!mPromise); +} + +bool RemoteWorkerController::PendingSWFetchEventOp::MaybeStart( + RemoteWorkerController* const aOwner) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + MOZ_ASSERT(aOwner); + + if (NS_WARN_IF(aOwner->mState == RemoteWorkerController::eTerminated)) { + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; + // Because the worker has transitioned to terminated, this operation is moot + // and so we should return true because there's no need to queue it. + return true; + } + + // The target content process must still be starting up. + if (!aOwner->mActor) { + MOZ_ASSERT(aOwner->mState == RemoteWorkerController::ePending); + return false; + } + + // At this point we are handing off responsibility for the promise to the + // actor. + FetchEventOpProxyParent::Create(aOwner->mActor.get(), std::move(mPromise), + mArgs, std::move(mReal), + std::move(mBodyStream)); + + return true; +} + +void RemoteWorkerController::PendingSWFetchEventOp::Cancel() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mPromise); + + if (mPromise) { + mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromise = nullptr; + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerController.h b/dom/workers/remoteworkers/RemoteWorkerController.h new file mode 100644 index 0000000000..af53634abd --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerController.h @@ -0,0 +1,323 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerController_h +#define mozilla_dom_RemoteWorkerController_h + +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/dom/ServiceWorkerOpArgs.h" +#include "mozilla/dom/ServiceWorkerOpPromise.h" + +namespace mozilla::dom { + +/* Here's a graph about this remote workers are spawned. + * + * _________________________________ | ________________________________ + * | | | | | + * | Parent process | IPC | Creation of Process X | + * | PBackground thread | | | | + * | | | | [RemoteWorkerService::Init()] | + * | | | | | | + * | | | | | (1) | + * | [RemoteWorkerManager:: (2) | | | V | + * | RegisterActor()]<-------- [new RemoteWorkerServiceChild] | + * | | | | | + * | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | | |________________________________| + * | | | + * | new SharedWorker/ServiceWorker | | + * | | ^ | IPC + * | (3) | (4)| | + * | V | | | + * | [RemoteWorkerController:: | | + * | | Create(data)] | | + * | | (5) | | + * | V | | + * | [RemoteWorkerManager::Launch()] | | + * | | | IPC _____________________________ + * | | (6) | | | | + * | | | | Selected content process | + * | V | (7) | | + * | [SendPRemoteWorkerConstructor()]--------->[new RemoteWorkerChild()] | + * | | | | | | | + * | | (8) | | | | | + * | V | | | V | + * | [RemoteWorkerController-> | | | RemoteWorkerChild->Exec() | + * | | SetControllerActor()] | | |_____________________________| + * | (9) | | IPC + * | V | | + * | [RemoteWorkerObserver-> | | + * | CreationCompleted()] | | + * |_________________________________| | + * | + * + * 1. When a new process starts, it creates a RemoteWorkerService singleton. + * This service creates a new thread (Worker Launcher) and from there, it + * starts a PBackground RemoteWorkerServiceChild actor. + * 2. On the parent process, PBackground thread, RemoteWorkerServiceParent + * actors are registered into the RemoteWorkerManager service. + * + * 3. At some point, a SharedWorker or a ServiceWorker must be executed. + * RemoteWorkerController::Create() is used to start the launching. This + * method must be called on the parent process, on the PBackground thread. + * 4. RemoteWorkerController object is immediately returned to the caller. Any + * operation done with this controller object will be stored in a queue, + * until the launching is correctly executed. + * 5. RemoteWorkerManager has the list of active RemoteWorkerServiceParent + * actors. From them, it picks one. + * In case we don't have any content process to select, a new one is + * spawned. If this happens, the operation is suspended until a new + * RemoteWorkerServiceParent is registered. + * 6. RemoteWorkerServiceParent is used to create a RemoteWorkerParent. + * 7. RemoteWorkerChild is created on a selected process and it executes the + * WorkerPrivate. + * 8. The RemoteWorkerParent actor is passed to the RemoteWorkerController. + * 9. RemoteWorkerController now is ready to continue and it called + * RemoteWorkerObserver to inform that the operation is completed. + * In case there were pending operations, they are now executed. + */ + +class ErrorValue; +class FetchEventOpParent; +class RemoteWorkerControllerParent; +class RemoteWorkerData; +class RemoteWorkerManager; +class RemoteWorkerParent; + +class RemoteWorkerObserver { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void CreationFailed() = 0; + + virtual void CreationSucceeded() = 0; + + virtual void ErrorReceived(const ErrorValue& aValue) = 0; + + virtual void LockNotified(bool aCreated) = 0; + + virtual void WebTransportNotified(bool aCreated) = 0; + + virtual void Terminated() = 0; +}; + +/** + * PBackground instance created by static RemoteWorkerController::Create that + * builds on RemoteWorkerManager. Interface to control the remote worker as well + * as receive events via the RemoteWorkerObserver interface that the owner + * (SharedWorkerManager in this case) must implement to hear about errors, + * termination, and whether the initial spawning succeeded/failed. + * + * Its methods may be called immediately after creation even though the worker + * is created asynchronously; an internal operation queue makes this work. + * Communicates with the remote worker via owned RemoteWorkerParent over + * PRemoteWorker protocol. + */ +class RemoteWorkerController final { + friend class RemoteWorkerControllerParent; + friend class RemoteWorkerManager; + friend class RemoteWorkerParent; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerController) + + static already_AddRefed<RemoteWorkerController> Create( + const RemoteWorkerData& aData, RemoteWorkerObserver* aObserver, + base::ProcessId = 0); + + void AddWindowID(uint64_t aWindowID); + + void RemoveWindowID(uint64_t aWindowID); + + void AddPortIdentifier(const MessagePortIdentifier& aPortIdentifier); + + void Terminate(); + + void Suspend(); + + void Resume(); + + void Freeze(); + + void Thaw(); + + RefPtr<ServiceWorkerOpPromise> ExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs); + + RefPtr<ServiceWorkerFetchEventOpPromise> ExecServiceWorkerFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<FetchEventOpParent> aReal); + + RefPtr<GenericPromise> SetServiceWorkerSkipWaitingFlag() const; + + bool IsTerminated() const; + + void NotifyWebTransport(bool aCreated); + + private: + RemoteWorkerController(const RemoteWorkerData& aData, + RemoteWorkerObserver* aObserver); + + ~RemoteWorkerController(); + + void SetWorkerActor(RemoteWorkerParent* aActor); + + void NoteDeadWorkerActor(); + + void ErrorPropagation(const ErrorValue& aValue); + + void NotifyLock(bool aCreated); + + void WorkerTerminated(); + + void Shutdown(); + + void CreationFailed(); + + void CreationSucceeded(); + + void CancelAllPendingOps(); + + template <typename... Args> + void MaybeStartSharedWorkerOp(Args&&... aArgs); + + void NoteDeadWorker(); + + RefPtr<RemoteWorkerObserver> mObserver; + RefPtr<RemoteWorkerParent> mActor; + + enum { + ePending, + eReady, + eTerminated, + } mState; + + const bool mIsServiceWorker; + + /** + * `PendingOp` is responsible for encapsulating logic for starting and + * canceling pending remote worker operations, as this logic may vary + * depending on the type of the remote worker and the type of the operation. + */ + class PendingOp { + public: + PendingOp() = default; + + PendingOp(const PendingOp&) = delete; + + PendingOp& operator=(const PendingOp&) = delete; + + virtual ~PendingOp() = default; + + /** + * Returns `true` if execution has started or the operation is moot and + * doesn't need to be queued, `false` if execution hasn't started and the + * operation should be queued. In general, operations should only return + * false when a remote worker is first starting up. Operations may also + * somewhat non-intuitively return true without doing anything if the worker + * has already been told to shutdown. + * + * Starting execution may depend the state of `aOwner.` + */ + virtual bool MaybeStart(RemoteWorkerController* const aOwner) = 0; + + /** + * Invoked if the operation will never have MaybeStart() called again + * because the RemoteWorkerController has terminated (or will never start). + * This should be used by PendingOps to clean up any resources they own and + * may also be called internally by their MaybeStart() methods if they + * determine the worker has been terminated. This should be idempotent. + */ + virtual void Cancel() = 0; + }; + + class PendingSharedWorkerOp final : public PendingOp { + public: + enum Type { + eTerminate, + eSuspend, + eResume, + eFreeze, + eThaw, + ePortIdentifier, + eAddWindowID, + eRemoveWindowID, + }; + + explicit PendingSharedWorkerOp(Type aType, uint64_t aWindowID = 0); + + explicit PendingSharedWorkerOp( + const MessagePortIdentifier& aPortIdentifier); + + ~PendingSharedWorkerOp(); + + bool MaybeStart(RemoteWorkerController* const aOwner) override; + + void Cancel() override; + + private: + const Type mType; + const MessagePortIdentifier mPortIdentifier; + const uint64_t mWindowID = 0; + bool mCompleted = false; + }; + + class PendingServiceWorkerOp final : public PendingOp { + public: + PendingServiceWorkerOp(ServiceWorkerOpArgs&& aArgs, + RefPtr<ServiceWorkerOpPromise::Private> aPromise); + + ~PendingServiceWorkerOp(); + + bool MaybeStart(RemoteWorkerController* const aOwner) override; + + void Cancel() override; + + private: + ServiceWorkerOpArgs mArgs; + RefPtr<ServiceWorkerOpPromise::Private> mPromise; + }; + + /** + * Custom pending op type to deal with the complexities of FetchEvents having + * their own actor. + * + * FetchEvent Ops have their own actor type because their lifecycle is more + * complex than IPDL's async return value mechanism allows. Additionally, + * its IPC struct potentially has to serialize RemoteLazyStreams which + * requires us to hold an nsIInputStream when at rest and serialize it when + * eventually sending. + */ + class PendingSWFetchEventOp final : public PendingOp { + public: + PendingSWFetchEventOp( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs, + RefPtr<ServiceWorkerFetchEventOpPromise::Private> aPromise, + RefPtr<FetchEventOpParent>&& aReal); + + ~PendingSWFetchEventOp(); + + bool MaybeStart(RemoteWorkerController* const aOwner) override; + + void Cancel() override; + + private: + ParentToParentServiceWorkerFetchEventOpArgs mArgs; + RefPtr<ServiceWorkerFetchEventOpPromise::Private> mPromise; + RefPtr<FetchEventOpParent> mReal; + nsCOMPtr<nsIInputStream> mBodyStream; + }; + + nsTArray<UniquePtr<PendingOp>> mPendingOps; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerController_h diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerChild.cpp b/dom/workers/remoteworkers/RemoteWorkerControllerChild.cpp new file mode 100644 index 0000000000..5a1db77cf0 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerChild.cpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerControllerChild.h" + +#include <utility> + +#include "MainThreadUtils.h" +#include "nsError.h" +#include "nsThreadUtils.h" + +#include "ServiceWorkerPrivate.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/PFetchEventOpChild.h" + +namespace mozilla { + +using ipc::IPCResult; + +namespace dom { + +RemoteWorkerControllerChild::RemoteWorkerControllerChild( + RefPtr<RemoteWorkerObserver> aObserver) + : mObserver(std::move(aObserver)) { + AssertIsOnMainThread(); + mRemoteWorkerLaunchStart = TimeStamp::Now(); + MOZ_ASSERT(mObserver); +} + +PFetchEventOpChild* RemoteWorkerControllerChild::AllocPFetchEventOpChild( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) { + MOZ_CRASH("PFetchEventOpChild actors must be manually constructed!"); + return nullptr; +} + +TimeStamp RemoteWorkerControllerChild::GetRemoteWorkerLaunchStart() { + MOZ_ASSERT(mRemoteWorkerLaunchStart); + return mRemoteWorkerLaunchStart; +} + +TimeStamp RemoteWorkerControllerChild::GetRemoteWorkerLaunchEnd() { + MOZ_ASSERT(mRemoteWorkerLaunchEnd); + return mRemoteWorkerLaunchEnd; +} + +bool RemoteWorkerControllerChild::DeallocPFetchEventOpChild( + PFetchEventOpChild* aActor) { + AssertIsOnMainThread(); + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +void RemoteWorkerControllerChild::ActorDestroy(ActorDestroyReason aReason) { + AssertIsOnMainThread(); + + mIPCActive = false; + + if (NS_WARN_IF(mObserver)) { + mObserver->ErrorReceived(NS_ERROR_DOM_ABORT_ERR); + } +} + +IPCResult RemoteWorkerControllerChild::RecvCreationFailed() { + AssertIsOnMainThread(); + + if (mObserver) { + mObserver->CreationFailed(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvCreationSucceeded() { + AssertIsOnMainThread(); + mRemoteWorkerLaunchEnd = TimeStamp::Now(); + + if (mObserver) { + mObserver->CreationSucceeded(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvErrorReceived( + const ErrorValue& aError) { + AssertIsOnMainThread(); + mRemoteWorkerLaunchEnd = TimeStamp::Now(); + + if (mObserver) { + mObserver->ErrorReceived(aError); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvTerminated() { + AssertIsOnMainThread(); + + if (mObserver) { + mObserver->Terminated(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerChild::RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve) { + AssertIsOnMainThread(); + + if (mObserver) { + static_cast<ServiceWorkerPrivate*>(mObserver.get()) + ->SetSkipWaitingFlag() + ->Then(GetCurrentSerialEventTarget(), __func__, + [resolve = std::move(aResolve)]( + const GenericPromise::ResolveOrRejectValue& aResult) { + resolve(aResult.IsResolve() ? aResult.ResolveValue() : false); + }); + + return IPC_OK(); + } + + aResolve(false); + + return IPC_OK(); +} + +void RemoteWorkerControllerChild::RevokeObserver( + RemoteWorkerObserver* aObserver) { + AssertIsOnMainThread(); + MOZ_ASSERT(aObserver); + MOZ_ASSERT(aObserver == mObserver); + + mObserver = nullptr; +} + +void RemoteWorkerControllerChild::MaybeSendDelete() { + AssertIsOnMainThread(); + + if (!mIPCActive) { + return; + } + + RefPtr<RemoteWorkerControllerChild> self = this; + + SendShutdown()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = std::move(self)](const ShutdownPromise::ResolveOrRejectValue&) { + if (self->mIPCActive) { + Unused << self->Send__delete__(self); + } + }); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerChild.h b/dom/workers/remoteworkers/RemoteWorkerControllerChild.h new file mode 100644 index 0000000000..283fff3916 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerChild.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_remoteworkercontrollerchild_h__ +#define mozilla_dom_remoteworkercontrollerchild_h__ + +#include "nsISupportsImpl.h" + +#include "RemoteWorkerController.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/PRemoteWorkerControllerChild.h" + +namespace mozilla::dom { + +/** + * Parent-process main-thread proxy used by ServiceWorkerManager to control + * RemoteWorkerController instances on the parent-process PBackground thread. + */ +class RemoteWorkerControllerChild final : public PRemoteWorkerControllerChild { + friend class PRemoteWorkerControllerChild; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerControllerChild, override) + + explicit RemoteWorkerControllerChild(RefPtr<RemoteWorkerObserver> aObserver); + + void Initialize(); + + void RevokeObserver(RemoteWorkerObserver* aObserver); + + void MaybeSendDelete(); + + TimeStamp GetRemoteWorkerLaunchStart(); + TimeStamp GetRemoteWorkerLaunchEnd(); + + private: + ~RemoteWorkerControllerChild() = default; + + PFetchEventOpChild* AllocPFetchEventOpChild( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs); + + bool DeallocPFetchEventOpChild(PFetchEventOpChild* aActor); + + void ActorDestroy(ActorDestroyReason aReason) override; + + mozilla::ipc::IPCResult RecvCreationFailed(); + + mozilla::ipc::IPCResult RecvCreationSucceeded(); + + mozilla::ipc::IPCResult RecvErrorReceived(const ErrorValue& aError); + + mozilla::ipc::IPCResult RecvTerminated(); + + mozilla::ipc::IPCResult RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve); + + RefPtr<RemoteWorkerObserver> mObserver; + + bool mIPCActive = true; + + TimeStamp mRemoteWorkerLaunchStart; + TimeStamp mRemoteWorkerLaunchEnd; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_remoteworkercontrollerchild_h__ diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp b/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp new file mode 100644 index 0000000000..7ac901f655 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp @@ -0,0 +1,215 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerControllerParent.h" + +#include <utility> + +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsThreadUtils.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/FetchEventOpParent.h" +#include "mozilla/dom/RemoteWorkerParent.h" +#include "mozilla/dom/ServiceWorkerOpPromise.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +RemoteWorkerControllerParent::RemoteWorkerControllerParent( + const RemoteWorkerData& aRemoteWorkerData) + : mRemoteWorkerController(RemoteWorkerController::Create( + aRemoteWorkerData, this, 0 /* random process ID */)) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mRemoteWorkerController); +} + +RefPtr<RemoteWorkerParent> RemoteWorkerControllerParent::GetRemoteWorkerParent() + const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mRemoteWorkerController); + + return mRemoteWorkerController->mActor; +} + +void RemoteWorkerControllerParent::MaybeSendSetServiceWorkerSkipWaitingFlag( + std::function<void(bool)>&& aCallback) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aCallback); + + if (!mIPCActive) { + aCallback(false); + return; + } + + SendSetServiceWorkerSkipWaitingFlag()->Then( + GetCurrentSerialEventTarget(), __func__, + [callback = std::move(aCallback)]( + const SetServiceWorkerSkipWaitingFlagPromise::ResolveOrRejectValue& + aResult) { + callback(aResult.IsResolve() ? aResult.ResolveValue() : false); + }); +} + +RemoteWorkerControllerParent::~RemoteWorkerControllerParent() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mIPCActive); + MOZ_ASSERT(!mRemoteWorkerController); +} + +PFetchEventOpParent* RemoteWorkerControllerParent::AllocPFetchEventOpParent( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) { + AssertIsOnBackgroundThread(); + + RefPtr<FetchEventOpParent> actor = new FetchEventOpParent(); + return actor.forget().take(); +} + +IPCResult RemoteWorkerControllerParent::RecvPFetchEventOpConstructor( + PFetchEventOpParent* aActor, + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<FetchEventOpParent> realFetchOp = + static_cast<FetchEventOpParent*>(aActor); + mRemoteWorkerController->ExecServiceWorkerFetchEventOp(aArgs, realFetchOp) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [fetchOp = std::move(realFetchOp)]( + ServiceWorkerFetchEventOpPromise::ResolveOrRejectValue&& + aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + MOZ_ASSERT(NS_FAILED(aResult.RejectValue())); + Unused << fetchOp->Send__delete__(fetchOp, aResult.RejectValue()); + return; + } + + Unused << fetchOp->Send__delete__(fetchOp, aResult.ResolveValue()); + }); + + return IPC_OK(); +} + +bool RemoteWorkerControllerParent::DeallocPFetchEventOpParent( + PFetchEventOpParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<FetchEventOpParent> actor = + dont_AddRef(static_cast<FetchEventOpParent*>(aActor)); + return true; +} + +IPCResult RemoteWorkerControllerParent::RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIPCActive); + MOZ_ASSERT(mRemoteWorkerController); + + mRemoteWorkerController->ExecServiceWorkerOp(std::move(aArgs)) + ->Then(GetCurrentSerialEventTarget(), __func__, + [resolve = std::move(aResolve)]( + ServiceWorkerOpPromise::ResolveOrRejectValue&& aResult) { + if (NS_WARN_IF(aResult.IsReject())) { + MOZ_ASSERT(NS_FAILED(aResult.RejectValue())); + resolve(aResult.RejectValue()); + return; + } + + resolve(aResult.ResolveValue()); + }); + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerParent::RecvShutdown( + ShutdownResolver&& aResolve) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIPCActive); + MOZ_ASSERT(mRemoteWorkerController); + + mIPCActive = false; + + mRemoteWorkerController->Shutdown(); + mRemoteWorkerController = nullptr; + + aResolve(true); + + return IPC_OK(); +} + +IPCResult RemoteWorkerControllerParent::Recv__delete__() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mIPCActive); + MOZ_ASSERT(!mRemoteWorkerController); + + return IPC_OK(); +} + +void RemoteWorkerControllerParent::ActorDestroy(ActorDestroyReason aReason) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mIPCActive)) { + mIPCActive = false; + } + + if (NS_WARN_IF(mRemoteWorkerController)) { + mRemoteWorkerController->Shutdown(); + mRemoteWorkerController = nullptr; + } +} + +void RemoteWorkerControllerParent::CreationFailed() { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendCreationFailed(); +} + +void RemoteWorkerControllerParent::CreationSucceeded() { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendCreationSucceeded(); +} + +void RemoteWorkerControllerParent::ErrorReceived(const ErrorValue& aValue) { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendErrorReceived(aValue); +} + +void RemoteWorkerControllerParent::Terminated() { + AssertIsOnBackgroundThread(); + + if (!mIPCActive) { + return; + } + + Unused << SendTerminated(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerControllerParent.h b/dom/workers/remoteworkers/RemoteWorkerControllerParent.h new file mode 100644 index 0000000000..618fbf9506 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerControllerParent.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_remoteworkercontrollerparent_h__ +#define mozilla_dom_remoteworkercontrollerparent_h__ + +#include <functional> + +#include "nsISupportsImpl.h" + +#include "RemoteWorkerController.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/PRemoteWorkerControllerParent.h" + +namespace mozilla::dom { + +/** + * PBackground-resident proxy used by ServiceWorkerManager because canonical + * ServiceWorkerManager state exists on the parent process main thread but the + * RemoteWorkerController API is used from the parent process PBackground + * thread. + */ +class RemoteWorkerControllerParent final : public PRemoteWorkerControllerParent, + public RemoteWorkerObserver { + friend class PRemoteWorkerControllerParent; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerControllerParent, override) + + explicit RemoteWorkerControllerParent( + const RemoteWorkerData& aRemoteWorkerData); + + // Returns the corresponding RemoteWorkerParent (if any). + RefPtr<RemoteWorkerParent> GetRemoteWorkerParent() const; + + void MaybeSendSetServiceWorkerSkipWaitingFlag( + std::function<void(bool)>&& aCallback); + + private: + ~RemoteWorkerControllerParent(); + + PFetchEventOpParent* AllocPFetchEventOpParent( + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs); + + mozilla::ipc::IPCResult RecvPFetchEventOpConstructor( + PFetchEventOpParent* aActor, + const ParentToParentServiceWorkerFetchEventOpArgs& aArgs) override; + + bool DeallocPFetchEventOpParent(PFetchEventOpParent* aActor); + + mozilla::ipc::IPCResult RecvExecServiceWorkerOp( + ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve); + + mozilla::ipc::IPCResult RecvShutdown(ShutdownResolver&& aResolve); + + mozilla::ipc::IPCResult Recv__delete__() override; + + void ActorDestroy(ActorDestroyReason aReason) override; + + void CreationFailed() override; + + void CreationSucceeded() override; + + void ErrorReceived(const ErrorValue& aValue) override; + + void LockNotified(bool aCreated) final { + // no-op for service workers + } + + void WebTransportNotified(bool aCreated) final { + // no-op for service workers + } + + void Terminated() override; + + RefPtr<RemoteWorkerController> mRemoteWorkerController; + + bool mIPCActive = true; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_remoteworkercontrollerparent_h__ diff --git a/dom/workers/remoteworkers/RemoteWorkerManager.cpp b/dom/workers/remoteworkers/RemoteWorkerManager.cpp new file mode 100644 index 0000000000..29577f0a34 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerManager.cpp @@ -0,0 +1,627 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerManager.h" + +#include <utility> + +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/ContentChild.h" // ContentChild::GetSingleton +#include "mozilla/dom/ProcessIsolation.h" +#include "mozilla/dom/RemoteWorkerController.h" +#include "mozilla/dom/RemoteWorkerParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "nsCOMPtr.h" +#include "nsImportModule.h" +#include "nsIXULRuntime.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "RemoteWorkerServiceParent.h" + +mozilla::LazyLogModule gRemoteWorkerManagerLog("RemoteWorkerManager"); + +#ifdef LOG +# undef LOG +#endif +#define LOG(fmt) \ + MOZ_LOG(gRemoteWorkerManagerLog, mozilla::LogLevel::Verbose, fmt) + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +// Raw pointer because this object is kept alive by RemoteWorkerServiceParent +// actors. +RemoteWorkerManager* sRemoteWorkerManager; + +bool IsServiceWorker(const RemoteWorkerData& aData) { + return aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData; +} + +void TransmitPermissionsAndBlobURLsForPrincipalInfo( + ContentParent* aContentParent, const PrincipalInfo& aPrincipalInfo) { + AssertIsOnMainThread(); + MOZ_ASSERT(aContentParent); + + auto principalOrErr = PrincipalInfoToPrincipal(aPrincipalInfo); + + if (NS_WARN_IF(principalOrErr.isErr())) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + aContentParent->TransmitBlobURLsForPrincipal(principal); + + MOZ_ALWAYS_SUCCEEDS( + aContentParent->TransmitPermissionsForPrincipal(principal)); +} + +} // namespace + +// static +bool RemoteWorkerManager::MatchRemoteType(const nsACString& processRemoteType, + const nsACString& workerRemoteType) { + LOG(("MatchRemoteType [processRemoteType=%s, workerRemoteType=%s]", + PromiseFlatCString(processRemoteType).get(), + PromiseFlatCString(workerRemoteType).get())); + + // Respecting COOP and COEP requires processing headers in the parent + // process in order to choose an appropriate content process, but the + // workers' ScriptLoader processes headers in content processes. An + // intermediary step that provides security guarantees is to simply never + // allow SharedWorkers and ServiceWorkers to exist in a COOP+COEP process. + // The ultimate goal is to allow these worker types to be put in such + // processes based on their script response headers. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1595206 + // + // RemoteWorkerManager::GetRemoteType should not select this remoteType + // and so workerRemoteType is not expected to be set to a coop+coep + // remoteType and here we can just assert that it is not happening. + MOZ_ASSERT(!IsWebCoopCoepRemoteType(workerRemoteType)); + + return processRemoteType.Equals(workerRemoteType); +} + +// static +Result<nsCString, nsresult> RemoteWorkerManager::GetRemoteType( + const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind) { + AssertIsOnMainThread(); + + MOZ_ASSERT_IF(aWorkerKind == WorkerKind::WorkerKindService, + aPrincipal->GetIsContentPrincipal()); + + // If E10S is fully disabled, there are no decisions to be made, and we need + // to finish the load in the parent process. + if (!BrowserTabsRemoteAutostart()) { + LOG(("GetRemoteType: Loading in parent process as e10s is disabled")); + return NOT_REMOTE_TYPE; + } + + nsCString preferredRemoteType = DEFAULT_REMOTE_TYPE; + if (aWorkerKind == WorkerKind::WorkerKindShared) { + if (auto* contentChild = ContentChild::GetSingleton()) { + // For a shared worker set the preferred remote type to the content + // child process remote type. + preferredRemoteType = contentChild->GetRemoteType(); + } else if (aPrincipal->IsSystemPrincipal()) { + preferredRemoteType = NOT_REMOTE_TYPE; + } + } + + auto result = IsolationOptionsForWorker( + aPrincipal, aWorkerKind, preferredRemoteType, FissionAutostart()); + if (NS_WARN_IF(result.isErr())) { + LOG(("GetRemoteType Abort: IsolationOptionsForWorker failed")); + return Err(NS_ERROR_DOM_ABORT_ERR); + } + auto options = result.unwrap(); + + if (MOZ_LOG_TEST(gRemoteWorkerManagerLog, LogLevel::Verbose)) { + nsCString principalOrigin; + aPrincipal->GetOrigin(principalOrigin); + + LOG( + ("GetRemoteType workerType=%s, principal=%s, " + "preferredRemoteType=%s, selectedRemoteType=%s", + aWorkerKind == WorkerKind::WorkerKindService ? "service" : "shared", + principalOrigin.get(), preferredRemoteType.get(), + options.mRemoteType.get())); + } + + return options.mRemoteType; +} + +// static +bool RemoteWorkerManager::HasExtensionPrincipal(const RemoteWorkerData& aData) { + auto principalInfo = aData.principalInfo(); + return principalInfo.type() == PrincipalInfo::TContentPrincipalInfo && + // This helper method is also called from the background thread and so + // we can't check if the principal does have an addonPolicy object + // associated and we have to resort to check the url scheme instead. + StringBeginsWith(principalInfo.get_ContentPrincipalInfo().spec(), + "moz-extension://"_ns); +} + +// static +bool RemoteWorkerManager::IsRemoteTypeAllowed(const RemoteWorkerData& aData) { + AssertIsOnMainThread(); + + // If Gecko is running in single process mode, there is no child process + // to select and we have to just consider it valid (if it should haven't + // been launched it should have been already prevented before reaching + // a RemoteWorkerChild instance). + if (!BrowserTabsRemoteAutostart()) { + return true; + } + + const auto& principalInfo = aData.principalInfo(); + + auto* contentChild = ContentChild::GetSingleton(); + if (!contentChild) { + // If e10s isn't disabled, only workers related to the system principal + // should be allowed to run in the parent process, and extension principals + // if extensions.webextensions.remote is false. + return principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || + (!StaticPrefs::extensions_webextensions_remote() && + aData.remoteType().Equals(NOT_REMOTE_TYPE) && + HasExtensionPrincipal(aData)); + } + + auto principalOrErr = PrincipalInfoToPrincipal(principalInfo); + if (NS_WARN_IF(principalOrErr.isErr())) { + return false; + } + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + // Recompute the remoteType based on the principal, to double-check that it + // has not been tempered to select a different child process than the one + // expected. + bool isServiceWorker = aData.serviceWorkerData().type() == + OptionalServiceWorkerData::TServiceWorkerData; + auto remoteType = GetRemoteType( + principal, isServiceWorker ? WorkerKindService : WorkerKindShared); + if (NS_WARN_IF(remoteType.isErr())) { + LOG(("IsRemoteTypeAllowed: Error to retrieve remote type")); + return false; + } + + return MatchRemoteType(remoteType.unwrap(), contentChild->GetRemoteType()); +} + +/* static */ +already_AddRefed<RemoteWorkerManager> RemoteWorkerManager::GetOrCreate() { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + if (!sRemoteWorkerManager) { + sRemoteWorkerManager = new RemoteWorkerManager(); + } + + RefPtr<RemoteWorkerManager> rwm = sRemoteWorkerManager; + return rwm.forget(); +} + +RemoteWorkerManager::RemoteWorkerManager() : mParentActor(nullptr) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!sRemoteWorkerManager); +} + +RemoteWorkerManager::~RemoteWorkerManager() { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(sRemoteWorkerManager == this); + sRemoteWorkerManager = nullptr; +} + +void RemoteWorkerManager::RegisterActor(RemoteWorkerServiceParent* aActor) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (!BackgroundParent::IsOtherProcessActor(aActor->Manager())) { + MOZ_ASSERT(!mParentActor); + mParentActor = aActor; + MOZ_ASSERT(mPendings.IsEmpty()); + return; + } + + MOZ_ASSERT(!mChildActors.Contains(aActor)); + mChildActors.AppendElement(aActor); + + if (!mPendings.IsEmpty()) { + const auto& processRemoteType = aActor->GetRemoteType(); + nsTArray<Pending> unlaunched; + + // Flush pending launching. + for (Pending& p : mPendings) { + if (p.mController->IsTerminated()) { + continue; + } + + const auto& workerRemoteType = p.mData.remoteType(); + + if (MatchRemoteType(processRemoteType, workerRemoteType)) { + LOG(("RegisterActor - Launch Pending, workerRemoteType=%s", + workerRemoteType.get())); + LaunchInternal(p.mController, aActor, p.mData); + } else { + unlaunched.AppendElement(std::move(p)); + continue; + } + } + + std::swap(mPendings, unlaunched); + + // AddRef is called when the first Pending object is added to mPendings, so + // the balancing Release is called when the last Pending object is removed. + // RemoteWorkerServiceParents will hold strong references to + // RemoteWorkerManager. + if (mPendings.IsEmpty()) { + Release(); + } + + LOG(("RegisterActor - mPendings length: %zu", mPendings.Length())); + } +} + +void RemoteWorkerManager::UnregisterActor(RemoteWorkerServiceParent* aActor) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + if (aActor == mParentActor) { + mParentActor = nullptr; + } else { + MOZ_ASSERT(mChildActors.Contains(aActor)); + mChildActors.RemoveElement(aActor); + } +} + +void RemoteWorkerManager::Launch(RemoteWorkerController* aController, + const RemoteWorkerData& aData, + base::ProcessId aProcessId) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + RemoteWorkerServiceParent* targetActor = SelectTargetActor(aData, aProcessId); + + // If there is not an available actor, let's store the data, and let's spawn a + // new process. + if (!targetActor) { + // If this is the first time we have a pending launching, we must keep alive + // the manager. + if (mPendings.IsEmpty()) { + AddRef(); + } + + Pending* pending = mPendings.AppendElement(); + pending->mController = aController; + pending->mData = aData; + + // Launching is async, so we cannot check for failures right here. + LaunchNewContentProcess(aData); + return; + } + + /** + * If a target actor for the remote worker has been selected, the actor has + * already been registered with the corresponding `ContentParent` and we + * should not increment the `mRemoteWorkerActorData`'s `mCount` again (see + * `SelectTargetActorForServiceWorker()` / + * `SelectTargetActorForSharedWorker()`). + */ + LaunchInternal(aController, targetActor, aData, true); +} + +void RemoteWorkerManager::LaunchInternal( + RemoteWorkerController* aController, + RemoteWorkerServiceParent* aTargetActor, const RemoteWorkerData& aData, + bool aRemoteWorkerAlreadyRegistered) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aController); + MOZ_ASSERT(aTargetActor); + MOZ_ASSERT(aTargetActor == mParentActor || + mChildActors.Contains(aTargetActor)); + + // We need to send permissions to content processes, but not if we're spawning + // the worker here in the parent process. + if (aTargetActor != mParentActor) { + RefPtr<ThreadsafeContentParentHandle> contentHandle = + BackgroundParent::GetContentParentHandle(aTargetActor->Manager()); + + // This won't cause any race conditions because the content process + // should wait for the permissions to be received before executing the + // Service Worker. + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [contentHandle = std::move(contentHandle), + principalInfo = aData.principalInfo()] { + AssertIsOnMainThread(); + if (RefPtr<ContentParent> contentParent = + contentHandle->GetContentParent()) { + TransmitPermissionsAndBlobURLsForPrincipalInfo(contentParent, + principalInfo); + } + }); + + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); + } + + RefPtr<RemoteWorkerParent> workerActor = MakeAndAddRef<RemoteWorkerParent>(); + if (!aTargetActor->Manager()->SendPRemoteWorkerConstructor(workerActor, + aData)) { + AsyncCreationFailed(aController); + return; + } + + workerActor->Initialize(aRemoteWorkerAlreadyRegistered); + + // This makes the link better the 2 actors. + aController->SetWorkerActor(workerActor); + workerActor->SetController(aController); +} + +void RemoteWorkerManager::AsyncCreationFailed( + RemoteWorkerController* aController) { + RefPtr<RemoteWorkerController> controller = aController; + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction("RemoteWorkerManager::AsyncCreationFailed", + [controller]() { controller->CreationFailed(); }); + + NS_DispatchToCurrentThread(r.forget()); +} + +template <typename Callback> +void RemoteWorkerManager::ForEachActor( + Callback&& aCallback, const nsACString& aRemoteType, + Maybe<base::ProcessId> aProcessId) const { + AssertIsOnBackgroundThread(); + + const auto length = mChildActors.Length(); + + auto end = static_cast<uint32_t>(rand()) % length; + if (aProcessId) { + // Start from the actor with the given processId instead of starting from + // a random index. + for (auto j = length - 1; j > 0; j--) { + if (mChildActors[j]->OtherPid() == *aProcessId) { + end = j; + break; + } + } + } + + uint32_t i = end; + + do { + MOZ_ASSERT(i < mChildActors.Length()); + RemoteWorkerServiceParent* actor = mChildActors[i]; + + if (MatchRemoteType(actor->GetRemoteType(), aRemoteType)) { + ThreadsafeContentParentHandle* contentHandle = + BackgroundParent::GetContentParentHandle(actor->Manager()); + + if (!aCallback(actor, contentHandle)) { + break; + } + } + + i = (i + 1) % length; + } while (i != end); +} + +/** + * When selecting a target actor for a given remote worker, we have to consider + * that: + * + * - Service Workers can spawn even when their registering page/script isn't + * active (e.g. push notifications), so we don't attempt to spawn the worker + * in its registering script's process. We search linearly and choose the + * search's starting position randomly. + * + * - When Fission is enabled, Shared Workers may have to be spawned into + * different child process from the one where it has been registered from, and + * that child process may be going to be marked as dead and shutdown. + * + * Spawning the workers in a random process makes the process selection criteria + * a little tricky, as a candidate process may imminently shutdown due to a + * remove worker actor unregistering + * (see `ContentParent::UnregisterRemoveWorkerActor`). + * + * In `ContentParent::MaybeBeginShutdown` we only dispatch a runnable + * to call `ContentParent::ShutDownProcess` if there are no registered remote + * worker actors, and we ensure that the check for the number of registered + * actors and the dispatching of the runnable are atomic. That happens on the + * main thread, so here on the background thread, while + * `ContentParent::mRemoteWorkerActorData` is locked, if `mCount` > 0, we can + * register a remote worker actor "early" and guarantee that the corresponding + * content process will not shutdown. + */ +RemoteWorkerServiceParent* RemoteWorkerManager::SelectTargetActorInternal( + const RemoteWorkerData& aData, base::ProcessId aProcessId) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mChildActors.IsEmpty()); + + RemoteWorkerServiceParent* actor = nullptr; + + const auto& workerRemoteType = aData.remoteType(); + + ForEachActor( + [&](RemoteWorkerServiceParent* aActor, + ThreadsafeContentParentHandle* aContentHandle) { + // Make sure to choose an actor related to a child process that is not + // going to shutdown while we are still in the process of launching the + // remote worker. + // + // ForEachActor will start from the child actor coming from the child + // process with a pid equal to aProcessId if any, otherwise it would + // start from a random actor in the mChildActors array, this guarantees + // that we will choose that actor if it does also match the remote type. + if (aContentHandle->MaybeRegisterRemoteWorkerActor( + [&](uint32_t count, bool shutdownStarted) -> bool { + return (count || !shutdownStarted) && + (aActor->OtherPid() == aProcessId || !actor); + })) { + actor = aActor; + return false; + } + MOZ_ASSERT(!actor); + return true; + }, + workerRemoteType, IsServiceWorker(aData) ? Nothing() : Some(aProcessId)); + + return actor; +} + +RemoteWorkerServiceParent* RemoteWorkerManager::SelectTargetActor( + const RemoteWorkerData& aData, base::ProcessId aProcessId) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + // System principal workers should run on the parent process. + if (aData.principalInfo().type() == PrincipalInfo::TSystemPrincipalInfo) { + MOZ_ASSERT(mParentActor); + return mParentActor; + } + + // Extension principal workers are allowed to run on the parent process + // when "extensions.webextensions.remote" pref is false. + if (aProcessId == base::GetCurrentProcId() && + aData.remoteType().Equals(NOT_REMOTE_TYPE) && + !StaticPrefs::extensions_webextensions_remote() && + HasExtensionPrincipal(aData)) { + MOZ_ASSERT(mParentActor); + return mParentActor; + } + + // If e10s is off, use the parent process. + if (!BrowserTabsRemoteAutostart()) { + MOZ_ASSERT(mParentActor); + return mParentActor; + } + + // We shouldn't have to worry about content-principal parent-process workers. + MOZ_ASSERT(aProcessId != base::GetCurrentProcId()); + + if (mChildActors.IsEmpty()) { + return nullptr; + } + + return SelectTargetActorInternal(aData, aProcessId); +} + +void RemoteWorkerManager::LaunchNewContentProcess( + const RemoteWorkerData& aData) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + nsCOMPtr<nsISerialEventTarget> bgEventTarget = GetCurrentSerialEventTarget(); + + using LaunchPromiseType = ContentParent::LaunchPromise; + using CallbackParamType = LaunchPromiseType::ResolveOrRejectValue; + + // A new content process must be requested on the main thread. On success, + // the success callback will also run on the main thread. On failure, however, + // the failure callback must be run on the background thread - it uses + // RemoteWorkerManager, and RemoteWorkerManager isn't threadsafe, so the + // promise callback will just dispatch the "real" failure callback to the + // background thread. + auto processLaunchCallback = [principalInfo = aData.principalInfo(), + bgEventTarget = std::move(bgEventTarget), + self = RefPtr<RemoteWorkerManager>(this)]( + const CallbackParamType& aValue, + const nsCString& remoteType) mutable { + if (aValue.IsResolve()) { + LOG(("LaunchNewContentProcess: successfully got child process")); + + // The failure callback won't run, and we're on the main thread, so + // we need to properly release the thread-unsafe RemoteWorkerManager. + NS_ProxyRelease(__func__, bgEventTarget, self.forget()); + } else { + // The "real" failure callback. + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [self = std::move(self), remoteType] { + nsTArray<Pending> uncancelled; + auto pendings = std::move(self->mPendings); + + for (const auto& pending : pendings) { + const auto& workerRemoteType = pending.mData.remoteType(); + if (self->MatchRemoteType(remoteType, workerRemoteType)) { + LOG( + ("LaunchNewContentProcess: Cancel pending with " + "workerRemoteType=%s", + workerRemoteType.get())); + pending.mController->CreationFailed(); + } else { + uncancelled.AppendElement(pending); + } + } + + std::swap(self->mPendings, uncancelled); + }); + + bgEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + } + }; + + LOG(("LaunchNewContentProcess: remoteType=%s", aData.remoteType().get())); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [callback = std::move(processLaunchCallback), + workerRemoteType = aData.remoteType()]() mutable { + auto remoteType = + workerRemoteType.IsEmpty() ? DEFAULT_REMOTE_TYPE : workerRemoteType; + + RefPtr<LaunchPromiseType> onFinished; + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + // Request a process making sure to specify aPreferUsed=true. For a + // given remoteType there's a pool size limit. If we pass aPreferUsed + // here, then if there's any process in the pool already, we will use + // that. If we pass false (which is the default if omitted), then + // this call will spawn a new process if the pool isn't at its limit + // yet. + // + // (Our intent is never to grow the pool size here. Our logic gets + // here because our current logic on PBackground is only aware of + // RemoteWorkerServiceParent actors that have registered themselves, + // which is fundamentally unaware of processes that will match in the + // future when they register. So we absolutely are fine with and want + // any existing processes.) + onFinished = ContentParent::GetNewOrUsedBrowserProcessAsync( + /* aRemoteType = */ remoteType, + /* aGroup */ nullptr, + hal::ProcessPriority::PROCESS_PRIORITY_FOREGROUND, + /* aPreferUsed */ true); + } else { + // We can find this event still in flight after having been asked to + // shutdown. Let's fake a failure to ensure our callback is called + // such that we clean up everything properly. + onFinished = LaunchPromiseType::CreateAndReject( + NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__); + } + onFinished->Then(GetCurrentSerialEventTarget(), __func__, + [callback = std::move(callback), + remoteType](const CallbackParamType& aValue) mutable { + callback(aValue, remoteType); + }); + }); + + SchedulerGroup::Dispatch(r.forget()); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerManager.h b/dom/workers/remoteworkers/RemoteWorkerManager.h new file mode 100644 index 0000000000..5ff11ee6e6 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerManager.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerManager_h +#define mozilla_dom_RemoteWorkerManager_h + +#include "base/process.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/RemoteWorkerTypes.h" +#include "mozilla/dom/WorkerPrivate.h" // WorkerKind enum +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class RemoteWorkerController; +class RemoteWorkerServiceParent; + +/** + * PBackground instance that keeps tracks of RemoteWorkerServiceParent actors + * (1 per process, including the main process) and pending + * RemoteWorkerController requests to spawn remote workers if the spawn request + * can't be immediately fulfilled. Decides which RemoteWorkerServerParent to use + * internally via SelectTargetActor in order to select a BackgroundParent + * manager on which to create a RemoteWorkerParent. + */ +class RemoteWorkerManager final { + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerManager) + + static already_AddRefed<RemoteWorkerManager> GetOrCreate(); + + void RegisterActor(RemoteWorkerServiceParent* aActor); + + void UnregisterActor(RemoteWorkerServiceParent* aActor); + + void Launch(RemoteWorkerController* aController, + const RemoteWorkerData& aData, base::ProcessId aProcessId); + + static bool MatchRemoteType(const nsACString& processRemoteType, + const nsACString& workerRemoteType); + + /** + * Get the child process RemoteType where a RemoteWorker should be + * launched. + */ + static Result<nsCString, nsresult> GetRemoteType( + const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind); + + /** + * Verify if a remote worker should be allowed to run in the current + * child process remoteType. + */ + static bool IsRemoteTypeAllowed(const RemoteWorkerData& aData); + + static bool HasExtensionPrincipal(const RemoteWorkerData& aData); + + private: + RemoteWorkerManager(); + ~RemoteWorkerManager(); + + RemoteWorkerServiceParent* SelectTargetActor(const RemoteWorkerData& aData, + base::ProcessId aProcessId); + + RemoteWorkerServiceParent* SelectTargetActorInternal( + const RemoteWorkerData& aData, base::ProcessId aProcessId) const; + + void LaunchInternal(RemoteWorkerController* aController, + RemoteWorkerServiceParent* aTargetActor, + const RemoteWorkerData& aData, + bool aRemoteWorkerAlreadyRegistered = false); + + void LaunchNewContentProcess(const RemoteWorkerData& aData); + + void AsyncCreationFailed(RemoteWorkerController* aController); + + // Iterate through all RemoteWorkerServiceParent actors with the given + // remoteType, starting from the actor related to a child process with pid + // aProcessId if needed and available or from a random index otherwise (as if + // iterating through a circular array). + // + // aCallback should be a invokable object with a function signature of + // bool (RemoteWorkerServiceParent*, RefPtr<ContentParent>&&) + // + // aCallback is called with the actor and corresponding ContentParent, should + // return false to abort iteration before all actors have been traversed (e.g. + // if the desired actor is found), and must not mutate mChildActors (which + // shouldn't be an issue because this function is const). aCallback also + // doesn't need to worry about proxy-releasing the ContentParent if it isn't + // moved out of the parameter. + template <typename Callback> + void ForEachActor(Callback&& aCallback, const nsACString& aRemoteType, + Maybe<base::ProcessId> aProcessId = Nothing()) const; + + // The list of existing RemoteWorkerServiceParent actors for child processes. + // Raw pointers because RemoteWorkerServiceParent actors unregister themselves + // when destroyed. + // XXX For Fission, where we could have a lot of child actors, should we maybe + // instead keep either a hash table (PID->actor) or perhaps store the actors + // in order, sorted by PID, to avoid linear lookup times? + nsTArray<RemoteWorkerServiceParent*> mChildActors; + RemoteWorkerServiceParent* mParentActor; + + struct Pending { + RefPtr<RemoteWorkerController> mController; + RemoteWorkerData mData; + }; + + nsTArray<Pending> mPendings; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerManager_h diff --git a/dom/workers/remoteworkers/RemoteWorkerParent.cpp b/dom/workers/remoteworkers/RemoteWorkerParent.cpp new file mode 100644 index 0000000000..1a9613b799 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerParent.cpp @@ -0,0 +1,200 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerParent.h" +#include "RemoteWorkerController.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/PFetchEventOpProxyParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Unused.h" +#include "nsProxyRelease.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +class UnregisterActorRunnable final : public Runnable { + public: + explicit UnregisterActorRunnable( + already_AddRefed<ThreadsafeContentParentHandle> aParent) + : Runnable("UnregisterActorRunnable"), mContentHandle(aParent) { + AssertIsOnBackgroundThread(); + } + + NS_IMETHOD + Run() override { + AssertIsOnMainThread(); + if (RefPtr<ContentParent> contentParent = + mContentHandle->GetContentParent()) { + contentParent->UnregisterRemoveWorkerActor(); + } + + return NS_OK; + } + + private: + RefPtr<ThreadsafeContentParentHandle> mContentHandle; +}; + +} // namespace + +RemoteWorkerParent::RemoteWorkerParent() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); +} + +RemoteWorkerParent::~RemoteWorkerParent() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); +} + +void RemoteWorkerParent::Initialize(bool aAlreadyRegistered) { + RefPtr<ThreadsafeContentParentHandle> parent = + BackgroundParent::GetContentParentHandle(Manager()); + + // Parent is null if the child actor runs on the parent process. + if (parent) { + if (!aAlreadyRegistered) { + parent->RegisterRemoteWorkerActor(); + } + + NS_ReleaseOnMainThread("RemoteWorkerParent::Initialize ContentParent", + parent.forget()); + } +} + +already_AddRefed<PFetchEventOpProxyParent> +RemoteWorkerParent::AllocPFetchEventOpProxyParent( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) { + MOZ_CRASH("PFetchEventOpProxyParent actors must be manually constructed!"); + return nullptr; +} + +void RemoteWorkerParent::ActorDestroy(IProtocol::ActorDestroyReason) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + RefPtr<ThreadsafeContentParentHandle> parent = + BackgroundParent::GetContentParentHandle(Manager()); + + // Parent is null if the child actor runs on the parent process. + if (parent) { + RefPtr<UnregisterActorRunnable> r = + new UnregisterActorRunnable(parent.forget()); + SchedulerGroup::Dispatch(r.forget()); + } + + if (mController) { + mController->NoteDeadWorkerActor(); + mController = nullptr; + } +} + +IPCResult RemoteWorkerParent::RecvCreated(const bool& aStatus) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!mController) { + return IPC_OK(); + } + + if (aStatus) { + mController->CreationSucceeded(); + } else { + mController->CreationFailed(); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerParent::RecvError(const ErrorValue& aValue) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->ErrorPropagation(aValue); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerParent::RecvNotifyLock(const bool& aCreated) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->NotifyLock(aCreated); + } + + return IPC_OK(); +} + +IPCResult RemoteWorkerParent::RecvNotifyWebTransport(const bool& aCreated) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->NotifyWebTransport(aCreated); + } + + return IPC_OK(); +} + +void RemoteWorkerParent::MaybeSendDelete() { + if (mDeleteSent) { + return; + } + + // For some reason, if the following two lines are swapped, ASan says there's + // a UAF... + mDeleteSent = true; + Unused << Send__delete__(this); +} + +IPCResult RemoteWorkerParent::RecvClose() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->WorkerTerminated(); + } + + MaybeSendDelete(); + + return IPC_OK(); +} + +void RemoteWorkerParent::SetController(RemoteWorkerController* aController) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + mController = aController; +} + +IPCResult RemoteWorkerParent::RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (mController) { + mController->SetServiceWorkerSkipWaitingFlag()->Then( + GetCurrentSerialEventTarget(), __func__, + [resolve = aResolve](bool /* unused */) { resolve(true); }, + [resolve = aResolve](nsresult /* unused */) { resolve(false); }); + } else { + aResolve(false); + } + + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerParent.h b/dom/workers/remoteworkers/RemoteWorkerParent.h new file mode 100644 index 0000000000..811206eb91 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerParent.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerParent_h +#define mozilla_dom_RemoteWorkerParent_h + +#include "mozilla/dom/PRemoteWorkerParent.h" + +namespace mozilla::dom { + +class RemoteWorkerController; + +/** + * PBackground-managed parent actor that is mutually associated with a single + * RemoteWorkerController. Relays error/close events to the controller and in + * turns is told life-cycle events. + */ +class RemoteWorkerParent final : public PRemoteWorkerParent { + friend class PRemoteWorkerParent; + + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerParent, override); + + RemoteWorkerParent(); + + void Initialize(bool aAlreadyRegistered = false); + + void SetController(RemoteWorkerController* aController); + + void MaybeSendDelete(); + + private: + ~RemoteWorkerParent(); + + already_AddRefed<PFetchEventOpProxyParent> AllocPFetchEventOpProxyParent( + const ParentToChildServiceWorkerFetchEventOpArgs& aArgs); + + void ActorDestroy(mozilla::ipc::IProtocol::ActorDestroyReason) override; + + mozilla::ipc::IPCResult RecvError(const ErrorValue& aValue); + + mozilla::ipc::IPCResult RecvNotifyLock(const bool& aCreated); + + mozilla::ipc::IPCResult RecvNotifyWebTransport(const bool& aCreated); + + mozilla::ipc::IPCResult RecvClose(); + + mozilla::ipc::IPCResult RecvCreated(const bool& aStatus); + + mozilla::ipc::IPCResult RecvSetServiceWorkerSkipWaitingFlag( + SetServiceWorkerSkipWaitingFlagResolver&& aResolve); + + bool mDeleteSent = false; + RefPtr<RemoteWorkerController> mController; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerParent_h diff --git a/dom/workers/remoteworkers/RemoteWorkerService.cpp b/dom/workers/remoteworkers/RemoteWorkerService.cpp new file mode 100644 index 0000000000..5a33160c04 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerService.cpp @@ -0,0 +1,345 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerService.h" + +#include "mozilla/dom/PRemoteWorkerParent.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "nsIObserverService.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "nsXPCOMPrivate.h" +#include "RemoteWorkerController.h" +#include "RemoteWorkerServiceChild.h" +#include "RemoteWorkerServiceParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +namespace { + +StaticMutex sRemoteWorkerServiceMutex; +StaticRefPtr<RemoteWorkerService> sRemoteWorkerService; + +} // namespace + +/** + * Block shutdown until the RemoteWorkers have shutdown so that we do not try + * and shutdown the RemoteWorkerService "Worker Launcher" thread until they have + * cleanly shutdown. + * + * Note that this shutdown blocker is not used to initiate shutdown of any of + * the workers directly; their shutdown is initiated from PBackground in the + * parent process. The shutdown blocker just exists to avoid races around + * shutting down the worker launcher thread after all of the workers have + * shutdown and torn down their actors. + * + * Currently, it should be the case that the ContentParent should want to keep + * the content processes alive until the RemoteWorkers have all reported their + * shutdown over IPC (on the "Worker Launcher" thread). So for an orderly + * content process shutdown that is waiting for there to no longer be a reason + * to keep the content process alive, this blocker should only hang around for + * a brief period of time, helping smooth out lifecycle edge cases. + * + * In the event the content process is trying to shutdown while the + * RemoteWorkers think they should still be alive, it's possible that this + * blocker could expose the relevant logic error in the parent process if no + * attempt is made to shutdown the RemoteWorker. + * + * ## Major Implementation Note: This is not actually an nsIAsyncShutdownClient + * + * Until https://bugzilla.mozilla.org/show_bug.cgi?id=1760855 provides us with a + * non-JS implementation of nsIAsyncShutdownService, this implementation + * actually uses event loop spinning. The patch on + * https://bugzilla.mozilla.org/show_bug.cgi?id=1775784 that changed us to use + * this hack can be reverted when the time is right. + * + * Event loop spinning is handled by `RemoteWorkerService::Observe` and it calls + * our exposed `ShouldBlockShutdown()` to know when to stop spinning. + */ +class RemoteWorkerServiceShutdownBlocker final { + ~RemoteWorkerServiceShutdownBlocker() = default; + + public: + explicit RemoteWorkerServiceShutdownBlocker(RemoteWorkerService* aService) + : mService(aService), mBlockShutdown(true) {} + + void RemoteWorkersAllGoneAllowShutdown() { + mService->FinishShutdown(); + mService = nullptr; + + mBlockShutdown = false; + } + + bool ShouldBlockShutdown() { return mBlockShutdown; } + + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerServiceShutdownBlocker); + + RefPtr<RemoteWorkerService> mService; + bool mBlockShutdown; +}; + +RemoteWorkerServiceKeepAlive::RemoteWorkerServiceKeepAlive( + RemoteWorkerServiceShutdownBlocker* aBlocker) + : mBlocker(aBlocker) { + MOZ_ASSERT(NS_IsMainThread()); +} + +RemoteWorkerServiceKeepAlive::~RemoteWorkerServiceKeepAlive() { + // Dispatch a runnable to the main thread to tell the Shutdown Blocker to + // remove itself and notify the RemoteWorkerService it can finish its + // shutdown. We dispatch this to the main thread even if we are already on + // the main thread. + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [blocker = std::move(mBlocker)] { + blocker->RemoteWorkersAllGoneAllowShutdown(); + }); + MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget())); +} + +/* static */ +void RemoteWorkerService::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + MOZ_ASSERT(!sRemoteWorkerService); + + RefPtr<RemoteWorkerService> service = new RemoteWorkerService(); + + // ## Content Process Initialization Case + // + // We are being told to initialize now that we know what our remote type is. + // Now is a fine time to call InitializeOnMainThread. + if (!XRE_IsParentProcess()) { + nsresult rv = service->InitializeOnMainThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + sRemoteWorkerService = service; + return; + } + // ## Parent Process Initialization Case + // + // Otherwise we are in the parent process and were invoked by + // nsLayoutStatics::Initialize. We wait until profile-after-change to kick + // off the Worker Launcher thread and have it connect to PBackground. This is + // an appropriate time for remote worker APIs to come online, especially + // because the PRemoteWorkerService mechanism needs processes to eagerly + // register themselves with PBackground since the design explicitly intends to + // avoid blocking on the main threads. (Disclaimer: Currently, things block + // on the main thread.) + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return; + } + + nsresult rv = obs->AddObserver(service, "profile-after-change", false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + sRemoteWorkerService = service; +} + +/* static */ +nsIThread* RemoteWorkerService::Thread() { + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + MOZ_ASSERT(sRemoteWorkerService); + MOZ_ASSERT(sRemoteWorkerService->mThread); + return sRemoteWorkerService->mThread; +} + +/* static */ +already_AddRefed<RemoteWorkerServiceKeepAlive> +RemoteWorkerService::MaybeGetKeepAlive() { + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + // In normal operation no one should be calling this without a service + // existing, so assert, but we'll also handle this being null as it is a + // plausible shutdown race. + MOZ_ASSERT(sRemoteWorkerService); + if (!sRemoteWorkerService) { + return nullptr; + } + + // Note that this value can be null, but this all handles that. + auto lockedKeepAlive = sRemoteWorkerService->mKeepAlive.Lock(); + RefPtr<RemoteWorkerServiceKeepAlive> extraRef = *lockedKeepAlive; + return extraRef.forget(); +} + +nsresult RemoteWorkerService::InitializeOnMainThread() { + // I would like to call this thread "DOM Remote Worker Launcher", but the max + // length is 16 chars. + nsresult rv = NS_NewNamedThread("Worker Launcher", getter_AddRefs(mThread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mShutdownBlocker = new RemoteWorkerServiceShutdownBlocker(this); + + { + RefPtr<RemoteWorkerServiceKeepAlive> keepAlive = + new RemoteWorkerServiceKeepAlive(mShutdownBlocker); + + auto lockedKeepAlive = mKeepAlive.Lock(); + *lockedKeepAlive = std::move(keepAlive); + } + + RefPtr<RemoteWorkerService> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "InitializeThread", [self]() { self->InitializeOnTargetThread(); }); + + rv = mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +RemoteWorkerService::RemoteWorkerService() + : mKeepAlive(nullptr, "RemoteWorkerService::mKeepAlive") { + MOZ_ASSERT(NS_IsMainThread()); +} + +RemoteWorkerService::~RemoteWorkerService() = default; + +void RemoteWorkerService::InitializeOnTargetThread() { + MOZ_ASSERT(mThread); + MOZ_ASSERT(mThread->IsOnCurrentThread()); + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return; + } + + RefPtr<RemoteWorkerServiceChild> serviceActor = + MakeAndAddRef<RemoteWorkerServiceChild>(); + if (NS_WARN_IF(!backgroundActor->SendPRemoteWorkerServiceConstructor( + serviceActor))) { + return; + } + + // Now we are ready! + mActor = serviceActor; +} + +void RemoteWorkerService::CloseActorOnTargetThread() { + MOZ_ASSERT(mThread); + MOZ_ASSERT(mThread->IsOnCurrentThread()); + + // If mActor is nullptr it means that initialization failed. + if (mActor) { + // Here we need to shutdown the IPC protocol. + mActor->Send__delete__(mActor); + mActor = nullptr; + } +} + +NS_IMETHODIMP +RemoteWorkerService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + MOZ_ASSERT(mThread); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + // Note that nsObserverList::NotifyObservers will hold a strong reference to + // our instance throughout the entire duration of this call, so it is not + // necessary for us to hold a kungFuDeathGrip here. + + // Drop our keep-alive. This could immediately result in our blocker saying + // it's okay for us to shutdown. SpinEventLoopUntil checks the predicate + // before spinning, so in the ideal case we will not spin the loop at all. + BeginShutdown(); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "RemoteWorkerService::Observe"_ns, + [&]() { return !mShutdownBlocker->ShouldBlockShutdown(); })); + + mShutdownBlocker = nullptr; + + return NS_OK; + } + + MOZ_ASSERT(!strcmp(aTopic, "profile-after-change")); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "profile-after-change"); + } + + return InitializeOnMainThread(); +} + +void RemoteWorkerService::BeginShutdown() { + // Drop our keepalive reference which may allow near-immediate removal of the + // blocker. + auto lockedKeepAlive = mKeepAlive.Lock(); + *lockedKeepAlive = nullptr; +} + +void RemoteWorkerService::FinishShutdown() { + // Clear the singleton before spinning the event loop when shutting down the + // thread so that MaybeGetKeepAlive() can assert if there are any late calls + // and to better reflect the actual state. + // + // Our caller, the RemoteWorkerServiceShutdownBlocker, will continue to hold a + // strong reference to us until we return from this call, so there are no + // lifecycle implications to dropping this reference. + { + StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); + sRemoteWorkerService = nullptr; + } + + RefPtr<RemoteWorkerService> self = this; + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction("RemoteWorkerService::CloseActorOnTargetThread", + [self]() { self->CloseActorOnTargetThread(); }); + + mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + + // We've posted a shutdown message; now shutdown the thread. This will spin + // a nested event loop waiting for the thread to process all pending events + // (including the just dispatched CloseActorOnTargetThread which will close + // the actor), ensuring to block main thread shutdown long enough to avoid + // races. + mThread->Shutdown(); + mThread = nullptr; +} + +NS_IMPL_ISUPPORTS(RemoteWorkerService, nsIObserver) + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerService.h b/dom/workers/remoteworkers/RemoteWorkerService.h new file mode 100644 index 0000000000..9e05e3958b --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerService.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerService_h +#define mozilla_dom_RemoteWorkerService_h + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/DataMutex.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsISupportsImpl.h" + +class nsIThread; + +namespace mozilla::dom { + +class RemoteWorkerService; +class RemoteWorkerServiceChild; +class RemoteWorkerServiceShutdownBlocker; + +/** + * Refcounted lifecycle helper; when its refcount goes to zero its destructor + * will call RemoteWorkerService::Shutdown() which will remove the shutdown + * blocker and shutdown the "Worker Launcher" thread. + * + * The RemoteWorkerService itself will hold a reference to this singleton which + * it will use to hand out additional refcounts to RemoteWorkerChild instances. + * When the shutdown blocker is notified that it's time to shutdown, the + * RemoteWorkerService's reference will be dropped. + */ +class RemoteWorkerServiceKeepAlive { + public: + explicit RemoteWorkerServiceKeepAlive( + RemoteWorkerServiceShutdownBlocker* aBlocker); + + private: + ~RemoteWorkerServiceKeepAlive(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteWorkerServiceKeepAlive); + + RefPtr<RemoteWorkerServiceShutdownBlocker> mBlocker; +}; + +/** + * Every process has a RemoteWorkerService which does the actual spawning of + * RemoteWorkerChild instances. The RemoteWorkerService creates a "Worker + * Launcher" thread at initialization on which it creates a + * RemoteWorkerServiceChild to service spawn requests. The thread is exposed as + * RemoteWorkerService::Thread(). A new/distinct thread is used because we + * (eventually) don't want to deal with main-thread contention, content + * processes have no equivalent of a PBackground thread, and actors are bound to + * specific threads. + * + * (Disclaimer: currently most RemoteWorkerOps need to happen on the main thread + * because the main-thread ends up as the owner of the worker and all + * manipulation of the worker must happen from the owning thread.) + */ +class RemoteWorkerService final : public nsIObserver { + friend class RemoteWorkerServiceShutdownBlocker; + friend class RemoteWorkerServiceKeepAlive; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + // To be called when a process is initialized on main-thread. + static void Initialize(); + + static nsIThread* Thread(); + + // Called by RemoteWorkerChild instances on the "Worker Launcher" thread at + // their creation to assist in tracking when it's safe to shutdown the + // RemoteWorkerService and "Worker Launcher" thread. This method will return + // a null pointer if the RemoteWorkerService has already begun shutdown. + // + // This is somewhat awkwardly a static method because the RemoteWorkerChild + // instances are not managed by RemoteWorkerServiceChild, but instead are + // managed by PBackground(Child). So we either need to find the + // RemoteWorkerService via the hidden singleton or by having the + // RemoteWorkerChild use PBackgroundChild::ManagedPRemoteWorkerServiceChild() + // to locate the instance. We are choosing to use the singleton because we + // already need to acquire a mutex in the call regardless and the upcoming + // refactorings may want to start using new toplevel protocols and this will + // avoid requiring a change when that happens. + static already_AddRefed<RemoteWorkerServiceKeepAlive> MaybeGetKeepAlive(); + + private: + RemoteWorkerService(); + ~RemoteWorkerService(); + + nsresult InitializeOnMainThread(); + + void InitializeOnTargetThread(); + + void CloseActorOnTargetThread(); + + // Called by RemoteWorkerServiceShutdownBlocker when it's time to drop the + // RemoteWorkerServiceKeepAlive reference. + void BeginShutdown(); + + // Called by RemoteWorkerServiceShutdownBlocker when the blocker has been + // removed and it's safe to shutdown the "Worker Launcher" thread. + void FinishShutdown(); + + nsCOMPtr<nsIThread> mThread; + RefPtr<RemoteWorkerServiceChild> mActor; + // The keep-alive is set and cleared on the main thread but we will hand out + // additional references to it from the "Worker Launcher" thread, so it's + // appropriate to use a mutex. (Alternately we could have used a ThreadBound + // and set and cleared on the "Worker Launcher" thread, but that would + // involve more moving parts and could have complicated edge cases.) + DataMutex<RefPtr<RemoteWorkerServiceKeepAlive>> mKeepAlive; + // In order to poll the blocker to know when we can stop spinning the event + // loop at shutdown, we retain a reference to the blocker. + RefPtr<RemoteWorkerServiceShutdownBlocker> mShutdownBlocker; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerService_h diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceChild.cpp b/dom/workers/remoteworkers/RemoteWorkerServiceChild.cpp new file mode 100644 index 0000000000..100908064e --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceChild.cpp @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerServiceChild.h" +#include "RemoteWorkerController.h" + +namespace mozilla::dom { + +RemoteWorkerServiceChild::RemoteWorkerServiceChild() = default; + +RemoteWorkerServiceChild::~RemoteWorkerServiceChild() = default; + +} // namespace mozilla::dom diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceChild.h b/dom/workers/remoteworkers/RemoteWorkerServiceChild.h new file mode 100644 index 0000000000..41ae879b29 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceChild.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerServiceChild_h +#define mozilla_dom_RemoteWorkerServiceChild_h + +#include "mozilla/dom/PRemoteWorkerServiceChild.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +class RemoteWorkerController; +class RemoteWorkerData; + +/** + * "Worker Launcher"-thread child actor created by the RemoteWorkerService to + * register itself with the PBackground RemoteWorkerManager in the parent. + */ +class RemoteWorkerServiceChild final : public PRemoteWorkerServiceChild { + public: + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerServiceChild, final) + + RemoteWorkerServiceChild(); + + private: + ~RemoteWorkerServiceChild(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerServiceChild_h diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceParent.cpp b/dom/workers/remoteworkers/RemoteWorkerServiceParent.cpp new file mode 100644 index 0000000000..c7fd5ea6c8 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceParent.cpp @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "RemoteWorkerServiceParent.h" +#include "RemoteWorkerManager.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +RemoteWorkerServiceParent::RemoteWorkerServiceParent() + : mManager(RemoteWorkerManager::GetOrCreate()) {} + +RemoteWorkerServiceParent::~RemoteWorkerServiceParent() = default; + +void RemoteWorkerServiceParent::Initialize(const nsACString& aRemoteType) { + AssertIsOnBackgroundThread(); + mRemoteType = aRemoteType; + mManager->RegisterActor(this); +} + +void RemoteWorkerServiceParent::ActorDestroy(IProtocol::ActorDestroyReason) { + AssertIsOnBackgroundThread(); + mManager->UnregisterActor(this); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/RemoteWorkerServiceParent.h b/dom/workers/remoteworkers/RemoteWorkerServiceParent.h new file mode 100644 index 0000000000..31ed29a78c --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerServiceParent.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RemoteWorkerServiceParent_h +#define mozilla_dom_RemoteWorkerServiceParent_h + +#include "mozilla/dom/PRemoteWorkerServiceParent.h" +#include "mozilla/dom/RemoteType.h" + +namespace mozilla::dom { + +class RemoteWorkerManager; + +/** + * PBackground parent actor that registers with the PBackground + * RemoteWorkerManager and used to relay spawn requests. + */ +class RemoteWorkerServiceParent final : public PRemoteWorkerServiceParent { + public: + RemoteWorkerServiceParent(); + NS_INLINE_DECL_REFCOUNTING(RemoteWorkerServiceParent, override); + + void ActorDestroy(mozilla::ipc::IProtocol::ActorDestroyReason) override; + + void Initialize(const nsACString& aRemoteType); + + nsCString GetRemoteType() const { return mRemoteType; } + + private: + ~RemoteWorkerServiceParent(); + + RefPtr<RemoteWorkerManager> mManager; + nsCString mRemoteType = NOT_REMOTE_TYPE; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteWorkerServiceParent_h diff --git a/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh new file mode 100644 index 0000000000..8894450b72 --- /dev/null +++ b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include ClientIPCTypes; +include IPCServiceWorkerDescriptor; +include IPCServiceWorkerRegistrationDescriptor; +include PBackgroundSharedTypes; +include URIParams; +include DOMTypes; +include NeckoChannelParams; +include ProtocolTypes; + +include "mozilla/dom/ClientIPCUtils.h"; +include "mozilla/dom/ReferrerInfoUtils.h"; +include "mozilla/dom/WorkerIPCUtils.h"; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; +using mozilla::dom::RequestCredentials from "mozilla/dom/RequestBinding.h"; +using mozilla::StorageAccess from "mozilla/StorageAccess.h"; +using mozilla::OriginTrials from "mozilla/OriginTrialsIPCUtils.h"; +using mozilla::dom::WorkerType from "mozilla/dom/WorkerBinding.h"; + +namespace mozilla { +namespace dom { + +struct ServiceWorkerData { + IPCServiceWorkerDescriptor descriptor; + IPCServiceWorkerRegistrationDescriptor registrationDescriptor; + nsString cacheName; + uint32_t loadFlags; + nsString id; +}; + +union OptionalServiceWorkerData { + void_t; + ServiceWorkerData; +}; + +struct RemoteWorkerData +{ + // This should only be used for devtools. + nsString originalScriptURL; + + // It is important to pass these as URIParams instead of strings for blob + // URLs: they carry an additional bit of state with them (mIsRevoked) that + // gives us a chance to use them, even after they've been revoked. Because + // we're asynchronously calling into the parent process before potentially + // loading the worker, it is important to keep this state. Note that this + // isn't a panacea: once the URL has been revoked, it'll give the worker 5 + // seconds to actually load it; so it's possible to still fail to load the + // blob URL if it takes too long to do the round trip. + URIParams baseScriptURL; + URIParams resolvedScriptURL; + + nsString name; + WorkerType type; + RequestCredentials credentials; + + PrincipalInfo loadingPrincipalInfo; + PrincipalInfo principalInfo; + PrincipalInfo partitionedPrincipalInfo; + + bool useRegularPrincipal; + bool usingStorageAccess; + + CookieJarSettingsArgs cookieJarSettings; + + nsCString domain; + + bool isSecureContext; + + IPCClientInfo? clientInfo; + + nullable nsIReferrerInfo referrerInfo; + + StorageAccess storageAccess; + + bool isThirdPartyContextToTopWindow; + + bool shouldResistFingerprinting; + + uint64_t? overriddenFingerprintingSettings; + + OriginTrials originTrials; + + OptionalServiceWorkerData serviceWorkerData; + + nsID agentClusterId; + + // Child process remote type where the worker should only run on. + nsCString remoteType; +}; + +// ErrorData/ErrorDataNote correspond to WorkerErrorReport/WorkerErrorNote +// which in turn correspond to JSErrorReport/JSErrorNotes which allows JS to +// report complicated errors such as redeclarations that involve multiple +// distinct lines. For more generic error-propagation IPC structures, see bug +// 1357463 on making ErrorResult usable over IPC. + +struct ErrorDataNote { + uint32_t lineNumber; + uint32_t columnNumber; + nsString message; + nsString filename; +}; + +struct ErrorData { + bool isWarning; + uint32_t lineNumber; + uint32_t columnNumber; + nsString message; + nsString filename; + nsString line; + ErrorDataNote[] notes; +}; + +struct CSPViolation { + nsString json; +}; + +union ErrorValue { + nsresult; + ErrorData; + CSPViolation; + void_t; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/remoteworkers/moz.build b/dom/workers/remoteworkers/moz.build new file mode 100644 index 0000000000..9983b7dd10 --- /dev/null +++ b/dom/workers/remoteworkers/moz.build @@ -0,0 +1,45 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "RemoteWorkerChild.h", + "RemoteWorkerController.h", + "RemoteWorkerControllerChild.h", + "RemoteWorkerControllerParent.h", + "RemoteWorkerManager.h", + "RemoteWorkerParent.h", + "RemoteWorkerService.h", + "RemoteWorkerServiceChild.h", + "RemoteWorkerServiceParent.h", +] + +UNIFIED_SOURCES += [ + "RemoteWorkerChild.cpp", + "RemoteWorkerController.cpp", + "RemoteWorkerControllerChild.cpp", + "RemoteWorkerControllerParent.cpp", + "RemoteWorkerManager.cpp", + "RemoteWorkerParent.cpp", + "RemoteWorkerService.cpp", + "RemoteWorkerServiceChild.cpp", + "RemoteWorkerServiceParent.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/serviceworkers", + "/xpcom/build", +] + +IPDL_SOURCES += [ + "PRemoteWorker.ipdl", + "PRemoteWorkerController.ipdl", + "PRemoteWorkerService.ipdl", + "RemoteWorkerTypes.ipdlh", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" |