/* -*- 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 #include #include "mozilla/AlreadyAddRefed.h" #include "mozilla/Assertions.h" #include "mozilla/Logging.h" #include "mozilla/Mutex.h" #include "mozilla/RefPtr.h" #include "mozilla/ThreadSafety.h" #include "mozilla/WinHandleWatcher.h" #include "nsCOMPtr.h" #include "nsIRunnable.h" #include "nsISerialEventTarget.h" #include "nsISupportsImpl.h" #include "nsITargetShutdownTask.h" #include "nsIWeakReferenceUtils.h" #include "nsThreadUtils.h" mozilla::LazyLogModule sHWLog("HandleWatcher"); namespace mozilla { namespace details { struct WaitHandleDeleter { void operator()(PTP_WAIT waitHandle) { MOZ_LOG(sHWLog, LogLevel::Debug, ("Closing PTP_WAIT %p", waitHandle)); ::CloseThreadpoolWait(waitHandle); } }; } // namespace details using WaitHandlePtr = UniquePtr; // HandleWatcher::Impl // // The backing implementation of HandleWatcher is a PTP_WAIT, an OS-threadpool // wait-object. Windows doesn't actually create a new thread per wait-object; // OS-threadpool threads are assigned to wait-objects only when their associated // handle become signaled -- although explicit documentation of this fact is // somewhat obscurely placed. [1] // // Throughout this class, we use manual locking and unlocking guarded by Clang's // thread-safety warnings, rather than scope-based lock-guards. See `Replace()` // for an explanation and justification. // // [1]https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects#remarks class HandleWatcher::Impl final : public nsITargetShutdownTask { NS_DECL_THREADSAFE_ISUPPORTS public: Impl() = default; private: ~Impl() { MOZ_ASSERT(IsStopped()); } struct Data { // The watched handle and its callback. HANDLE handle; RefPtr target; nsCOMPtr runnable; // Handle to the threadpool wait-object. WaitHandlePtr waitHandle; // A pointer to ourselves, notionally owned by the wait-object. RefPtr self; // (We can't actually do this because a) it has annoying consequences in // C++20 thanks to P1008R1, and b) Clang just ignores it anyway.) // // ~Data() MOZ_EXCLUDES(mMutex) = default; }; mozilla::Mutex mMutex{"HandleWatcher::Impl"}; Data mData MOZ_GUARDED_BY(mMutex) = {}; // Callback from OS threadpool wait-object. static void CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE, void* ctx, PTP_WAIT aWaitHandle, TP_WAIT_RESULT aResult) { static_cast(ctx)->OnWaitCompleted(aWaitHandle, aResult); } void OnWaitCompleted(PTP_WAIT aWaitHandle, TP_WAIT_RESULT aResult) MOZ_EXCLUDES(mMutex) { MOZ_ASSERT(aResult == WAIT_OBJECT_0); mMutex.Lock(); // If this callback is no longer the active callback, skip out. // All cleanup is someone else's problem. if (aWaitHandle != mData.waitHandle.get()) { MOZ_LOG(sHWLog, LogLevel::Debug, ("Recv'd already-stopped callback: HW %p | PTP_WAIT %p", this, aWaitHandle)); mMutex.Unlock(); return; } // Take our self-pointer so that we release it on exit. RefPtr self = std::move(mData.self); MOZ_LOG(sHWLog, LogLevel::Info, ("Recv'd callback: HW %p | handle %p | target %p | PTP_WAIT %p", this, mData.handle, mData.target.get(), aWaitHandle)); // This may fail if (for example) `mData.target` is being shut down, but we // have not yet received the shutdown callback. mData.target->Dispatch(mData.runnable.forget()); Replace(Data{}); } public: static RefPtr Create(HANDLE aHandle, nsIEventTarget* aTarget, already_AddRefed aRunnable) { auto impl = MakeRefPtr(); bool const ok [[maybe_unused]] = impl->Watch(aHandle, aTarget, std::move(aRunnable)); MOZ_ASSERT(ok); return impl; } private: bool Watch(HANDLE aHandle, nsIEventTarget* aTarget, already_AddRefed aRunnable) MOZ_EXCLUDES(mMutex) { MOZ_ASSERT(aHandle); MOZ_ASSERT(aTarget); RefPtr target(aTarget); WaitHandlePtr waitHandle{ ::CreateThreadpoolWait(&WaitCallback, this, nullptr)}; if (!waitHandle) { return false; } { mMutex.Lock(); nsresult const ret = aTarget->RegisterShutdownTask(this); if (NS_FAILED(ret)) { mMutex.Unlock(); return false; } MOZ_LOG(sHWLog, LogLevel::Info, ("Setting callback: HW %p | handle %p | target %p | PTP_WAIT %p", this, aHandle, aTarget, waitHandle.get())); // returns `void`; presumably always succeeds given a successful // `::CreateThreadpoolWait()` ::SetThreadpoolWait(waitHandle.get(), aHandle, nullptr); // After this point, you must call `FlushWaitHandle(waitHandle.get())` // before destroying the wait handle. (Note that this must be done while // *not* holding `mMutex`!) Replace(Data{.handle = aHandle, .target = std::move(target), .runnable = aRunnable, .waitHandle = std::move(waitHandle), .self = this}); } return true; } void TargetShutdown() MOZ_EXCLUDES(mMutex) override final { mMutex.Lock(); MOZ_LOG(sHWLog, LogLevel::Debug, ("Target shutdown: HW %p | handle %p | target %p | PTP_WAIT %p", this, mData.handle, mData.target.get(), mData.waitHandle.get())); // Clear mData.target, since there's no need to unregister the shutdown task // anymore. Hold onto it until we release the mutex, though, to avoid any // reentrancy issues. // // This is more for internal consistency than safety: someone has to be // shutting `target` down, and that someone isn't us, so there's necessarily // another reference out there. (Although decrementing the refcount might // still have arbitrary effects if someone's been excessively clever with // nsISupports::Release...) auto const oldTarget = std::move(mData.target); Replace(Data{}); // (Static-assert that the mutex has indeed been released.) ([&]() MOZ_EXCLUDES(mMutex) {})(); } public: void Stop() MOZ_EXCLUDES(mMutex) { mMutex.Lock(); Replace(Data{}); } bool IsStopped() MOZ_EXCLUDES(mMutex) { mozilla::MutexAutoLock lock(mMutex); return !mData.handle; } private: // Throughout this class, we use manual locking and unlocking guarded by // Clang's thread-safety warnings, rather than scope-based lock-guards. This // is largely driven by `Replace()`, below, which performs both operations // which require the mutex to be held and operations which require it to not // be held, and therefore must explicitly sequence the mutex release. // // These explicit locks, unlocks, and annotations are both alien to C++ and // offensively tedious; but they _are_ still checked for state consistency at // scope boundaries. (The concerned reader is invited to test this by // deliberately removing an `mMutex.Unlock()` call from anywhere in the class // and viewing the resultant compiler diagnostics.) // // A more principled, or at least differently-principled, implementation might // create a scope-based lock-guard and pass it to `Replace()` to dispose of at // the proper time. Alas, it cannot be communicated to Clang's thread-safety // checker that such a guard is associated with `mMutex`. // void Replace(Data&& aData) MOZ_CAPABILITY_RELEASE(mMutex) { // either both handles are NULL, or neither is MOZ_ASSERT(!!aData.handle == !!aData.waitHandle); if (mData.handle) { MOZ_LOG(sHWLog, LogLevel::Info, ("Stop callback: HW %p | handle %p | target %p | PTP_WAIT %p", this, mData.handle, mData.target.get(), mData.waitHandle.get())); } if (mData.target) { mData.target->UnregisterShutdownTask(this); } // Extract the old data and insert the new -- but hold onto the old data for // now. (See [1] and [2], below.) Data oldData = std::exchange(mData, std::move(aData)); //////////////////////////////////////////////////////////////////////////// // Release the mutex. mMutex.Unlock(); //////////////////////////////////////////////////////////////////////////// // [1] `oldData.self` will be unset if the old callback already ran (or if // there was no old callback in the first place). If it's set, though, we // need to explicitly clear out the wait-object first. if (oldData.self) { MOZ_ASSERT(oldData.waitHandle); FlushWaitHandle(oldData.waitHandle.get()); } // [2] oldData also includes several other reference-counted pointers. It's // possible that these may be the last pointer to something, so releasing // them may have arbitrary side-effects -- like calling this->Stop(), which // will try to reacquire the mutex. // // Now that we've released the mutex, we can (implicitly) release them all // here. } // Either confirm as complete or cancel any callbacks on aWaitHandle. Block // until this is done. (See documentation for ::CloseThreadpoolWait().) void FlushWaitHandle(PTP_WAIT aWaitHandle) MOZ_EXCLUDES(mMutex) { ::SetThreadpoolWait(aWaitHandle, nullptr, nullptr); // This might block on `OnWaitCompleted()`, so we can't hold `mMutex` here. ::WaitForThreadpoolWaitCallbacks(aWaitHandle, TRUE); // ::CloseThreadpoolWait() itself is the caller's responsibility. } }; NS_IMPL_ISUPPORTS(HandleWatcher::Impl, nsITargetShutdownTask) ////// // HandleWatcher member function implementations HandleWatcher::HandleWatcher() : mImpl{} {} HandleWatcher::~HandleWatcher() { if (mImpl) { MOZ_ASSERT(mImpl->IsStopped()); mImpl->Stop(); // just in case, in release } } HandleWatcher::HandleWatcher(HandleWatcher&&) noexcept = default; HandleWatcher& HandleWatcher::operator=(HandleWatcher&&) noexcept = default; void HandleWatcher::Watch(HANDLE aHandle, nsIEventTarget* aTarget, already_AddRefed aRunnable) { auto impl = Impl::Create(aHandle, aTarget, std::move(aRunnable)); MOZ_ASSERT(impl); if (mImpl) { mImpl->Stop(); } mImpl = std::move(impl); } void HandleWatcher::Stop() { if (mImpl) { mImpl->Stop(); } } bool HandleWatcher::IsStopped() { return !mImpl || mImpl->IsStopped(); } } // namespace mozilla