Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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
*
* non-JS implementation of nsIAsyncShutdownService, this implementation
* actually uses event loop spinning. The patch on
* 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::InitializeParent() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(XRE_IsParentProcess());
StaticMutexAutoLock lock(sRemoteWorkerServiceMutex);
MOZ_ASSERT(!sRemoteWorkerService);
RefPtr<RemoteWorkerService> service = new RemoteWorkerService();
// ## 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 */
void RemoteWorkerService::InitializeChild(
mozilla::ipc::Endpoint<PRemoteWorkerServiceChild> aEndpoint) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!XRE_IsParentProcess());
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.
nsresult rv = service->InitializeOnMainThread(std::move(aEndpoint));
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(
mozilla::ipc::Endpoint<PRemoteWorkerServiceChild> aEndpoint) {
// 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, endpoint = std::move(aEndpoint)]() mutable {
self->InitializeOnTargetThread(std::move(endpoint));
});
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(
mozilla::ipc::Endpoint<PRemoteWorkerServiceChild> aEndpoint) {
MOZ_ASSERT(mThread);
MOZ_ASSERT(mThread->IsOnCurrentThread());
RefPtr<RemoteWorkerServiceChild> serviceActor =
MakeAndAddRef<RemoteWorkerServiceChild>();
if (NS_WARN_IF(!aEndpoint.Bind(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->Close();
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");
}
Endpoint<PRemoteWorkerServiceChild> childEp;
RefPtr<RemoteWorkerServiceParent> parentActor =
RemoteWorkerServiceParent::CreateForProcess(nullptr, &childEp);
NS_ENSURE_TRUE(parentActor, NS_ERROR_FAILURE);
return InitializeOnMainThread(std::move(childEp));
}
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