forked from mirrors/gecko-dev
		
	 371b0d83fc
			
		
	
	
		371b0d83fc
		
	
	
	
	
		
			
			Implement a mochitest confirming the stability of Firefox when the file-picker crashes (patterned loosely off the existing geolocation crash tests). Differential Revision: https://phabricator.services.mozilla.com/D184723
		
			
				
	
	
		
			306 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			306 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* -*- 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 <windows.h>
 | |
| #include <threadpoolapiset.h>
 | |
| 
 | |
| #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<TP_WAIT, details::WaitHandleDeleter>;
 | |
| 
 | |
| // 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<nsIEventTarget> target;
 | |
|     nsCOMPtr<nsIRunnable> runnable;
 | |
| 
 | |
|     // Handle to the threadpool wait-object.
 | |
|     WaitHandlePtr waitHandle;
 | |
|     // A pointer to ourselves, notionally owned by the wait-object.
 | |
|     RefPtr<Impl> 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<Impl*>(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<Impl> 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<Impl> Create(HANDLE aHandle, nsIEventTarget* aTarget,
 | |
|                              already_AddRefed<nsIRunnable> aRunnable) {
 | |
|     auto impl = MakeRefPtr<Impl>();
 | |
|     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<nsIRunnable> aRunnable) MOZ_EXCLUDES(mMutex) {
 | |
|     MOZ_ASSERT(aHandle);
 | |
|     MOZ_ASSERT(aTarget);
 | |
| 
 | |
|     RefPtr<nsIEventTarget> 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<nsIRunnable> 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
 |