/* -*- 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 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 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 r = NS_NewRunnableFunction(__func__, [blocker = std::move(mBlocker)] { blocker->RemoteWorkersAllGoneAllowShutdown(); }); MOZ_ALWAYS_SUCCEEDS( SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); } /* static */ void RemoteWorkerService::Initialize() { MOZ_ASSERT(NS_IsMainThread()); StaticMutexAutoLock lock(sRemoteWorkerServiceMutex); MOZ_ASSERT(!sRemoteWorkerService); RefPtr 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 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 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 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 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 keepAlive = new RemoteWorkerServiceKeepAlive(mShutdownBlocker); auto lockedKeepAlive = mKeepAlive.Lock(); *lockedKeepAlive = std::move(keepAlive); } RefPtr self = this; nsCOMPtr 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 serviceActor = MakeAndAddRef(); 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 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 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 self = this; nsCOMPtr 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