Bug 1293922 - TimerUtils for easier-to-use timers - r?froydnj draft
authorGerald Squelart <gsquelart@mozilla.com>
Tue, 25 Oct 2016 22:40:55 +1100
changeset 429180 59fbbfb2cfa266361ccc8ef583b65e1447b01714
parent 429179 c6ccd71126ff514bfc44b53e2217562e29a0cc38
child 429181 89fd20af72cba033677e61100bef417672319a78
push id33501
push usergsquelart@mozilla.com
push dateTue, 25 Oct 2016 11:42:25 +0000
reviewersfroydnj
bugs1293922
milestone52.0a1
Bug 1293922 - TimerUtils for easier-to-use timers - r?froydnj TimerUtils provides a number of classes that help with managing timers, from a simple handle class managing an nsCOMPtr<nsITimer> to classes that accept lambda callbacks and can safely auto-cancel themselves (if needed) on destruction. MozReview-Commit-ID: 5GzZlvhZOs2
xpcom/glue/TimerUtils.cpp
xpcom/glue/TimerUtils.h
xpcom/glue/moz.build
xpcom/glue/objs.mozbuild
xpcom/tests/TestTimers.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/glue/TimerUtils.cpp
@@ -0,0 +1,111 @@
+/* -*- 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 "TimerUtils.h"
+
+#include "nsComponentManagerUtils.h"
+
+nsresult
+TimerHandle::Create()
+{
+  nsresult error;
+  mTimerCOMPtr = do_CreateInstance(NS_TIMER_CONTRACTID, &error);
+  if (MOZ_UNLIKELY(NS_FAILED(error))) {
+    return error;
+  }
+  if (MOZ_UNLIKELY(!mTimerCOMPtr)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+  return NS_OK;
+}
+
+// Create a timer instance if there isn't already one.
+nsresult
+TimerHandle::Ensure()
+{
+  if (!mTimerCOMPtr) {
+    return Create();
+  }
+  return NS_OK;
+}
+
+nsresult
+TimerHandle::Cancel(void)
+{
+  if (mTimerCOMPtr) {
+    return mTimerCOMPtr->Cancel();
+  }
+  return NS_OK;
+}
+
+void
+TimerHandle::Release(void)
+{
+  mTimerCOMPtr = nullptr;
+}
+
+nsresult
+TimerHandle::CancelAndRelease(void)
+{
+  if (mTimerCOMPtr) {
+    nsresult rv = mTimerCOMPtr->Cancel();
+    mTimerCOMPtr = nullptr;
+    return rv;
+  }
+  return NS_OK;
+}
+
+nsresult
+TimerHandleWithFunction::Init(uint32_t aDelay_ms,
+                              uint32_t aType,
+                              mozilla::function<void(void)>&& aF)
+{
+  nsresult rv = Ensure();
+  if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+    return rv;
+  }
+  return get()->InitWithCallback(
+    new Callback(mozilla::Move(aF)),
+    aDelay_ms, aType);
+}
+
+enum CallbackSituation
+{
+  TimerFired,
+  ResourceDestroyed
+};
+
+nsresult
+TimerHandleWithFunction::InitWithCleanup(uint32_t aDelay_ms,
+                                         uint32_t aType,
+                                         mozilla::function<void(CallbackSituation)>&& aF)
+{
+  nsresult rv = Ensure();
+  if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+    return rv;
+  }
+  return get()->InitWithCallback(
+    new CallbackAndCleaner(mozilla::Move(aF)),
+    aDelay_ms, aType);
+}
+
+NS_IMPL_ISUPPORTS(TimerHandleWithFunction::Callback, nsITimerCallback)
+
+NS_IMETHODIMP
+TimerHandleWithFunction::Callback::Notify(nsITimer* aTimer)
+{
+  mF();
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(TimerHandleWithFunction::CallbackAndCleaner, nsITimerCallback)
+
+NS_IMETHODIMP
+TimerHandleWithFunction::CallbackAndCleaner::Notify(nsITimer* aTimer)
+{
+  mF(TimerFired);
+  return NS_OK;
+}
new file mode 100644
--- /dev/null
+++ b/xpcom/glue/TimerUtils.h
@@ -0,0 +1,143 @@
+/* -*- 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 TimerUtils_h___
+#define TimerUtils_h___
+
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+
+#include "mozilla/Function.h"
+
+// Encapsulates an nsCOMPtr<nsITimer>, with utility methods to create and access
+// the timer.
+class TimerHandle
+{
+public:
+  TimerHandle() = default;
+  TimerHandle(const TimerHandle&) = default;
+  TimerHandle& operator=(const TimerHandle&) = default;
+  TimerHandle(TimerHandle&&) = default;
+  TimerHandle& operator=(TimerHandle&&) = default;
+  ~TimerHandle() = default;
+
+  // Create a timer instance, override previous one if any.
+  nsresult Create();
+  // Create a timer instance if there isn't already one.
+  nsresult Ensure();
+  // Cancel timer, if any.
+  nsresult Cancel();
+  // Release timer from this handle.
+  void Release();
+  // If there is a timer, cancel it and then release it from this handle.
+  nsresult CancelAndRelease();
+
+  // nsCOMPtr-like API.
+  nsITimer* get() const { return mTimerCOMPtr.get(); }
+  explicit operator bool() const { return get(); }
+  nsITimer* operator->() const
+  {
+    MOZ_ASSERT(get() != 0,
+               "You can't dereference a NULL TimerHandle with operator->().");
+    return get();
+  }
+  nsITimer& operator*() const
+  {
+    MOZ_ASSERT(get() != 0,
+               "You can't dereference a NULL TimerHandle with operator*().");
+    return *get();
+  }
+
+private:
+  nsCOMPtr<nsITimer> mTimerCOMPtr;
+};
+
+// Timer handle that will cancel its timer when destroyed.
+// Useful when storing a timer in a class, when that timer callback works on
+// that class, to avoid UAF.
+class AutoCancelingTimerHandle : public TimerHandle
+{
+public:
+  AutoCancelingTimerHandle() = default;
+  AutoCancelingTimerHandle(const AutoCancelingTimerHandle&) = default;
+  AutoCancelingTimerHandle& operator=(const AutoCancelingTimerHandle&) = default;
+  AutoCancelingTimerHandle(AutoCancelingTimerHandle&&) = default;
+  AutoCancelingTimerHandle& operator=(AutoCancelingTimerHandle&&) = default;
+  ~AutoCancelingTimerHandle() { Cancel(); }
+};
+
+// Timer handle that takes a function object (e.g.: lambda) as callback.
+class TimerHandleWithFunction : public TimerHandle
+{
+public:
+  TimerHandleWithFunction() = default;
+  TimerHandleWithFunction(const TimerHandleWithFunction&) = default;
+  TimerHandleWithFunction& operator=(const TimerHandleWithFunction&) = default;
+  TimerHandleWithFunction(TimerHandleWithFunction&&) = default;
+  TimerHandleWithFunction& operator=(TimerHandleWithFunction&&) = default;
+  ~TimerHandleWithFunction() = default;
+
+  // Start a timer, and call aF() whenever the timer fires.
+  nsresult Init(uint32_t aDelay_ms,
+                uint32_t aType, // TYPE_* enum from nsITimer
+                mozilla::function<void(void)>&& aF);
+
+  enum CallbackSituation
+  {
+    TimerFired,
+    ResourceDestroyed
+  };
+
+  // Start a timer, aF will be called if/when the timer fires, and also when
+  // the timer resource is destroyed (e.g., when the timer is first cancelled,
+  // or destroyed).
+  nsresult InitWithCleanup(uint32_t aDelay_ms,
+                           uint32_t aType, // TYPE_* enum from nsITimer
+                           mozilla::function<void(CallbackSituation)>&& aF);
+
+private:
+  class Callback final : public nsITimerCallback
+  {
+  public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_DECL_NSITIMERCALLBACK
+    template <typename AF>
+    explicit Callback(AF&& aF) : mF(mozilla::Forward<AF>(aF)) {}
+  private:
+    virtual ~Callback() = default;
+    mozilla::function<void(void)> mF;
+  };
+
+  class CallbackAndCleaner final : public nsITimerCallback
+  {
+  public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_DECL_NSITIMERCALLBACK
+    template <typename AF>
+    explicit CallbackAndCleaner(AF&& aF) : mF(mozilla::Forward<AF>(aF)) {}
+  private:
+    virtual ~CallbackAndCleaner()
+    {
+      mF(ResourceDestroyed);
+    }
+    mozilla::function<void(CallbackSituation)> mF;
+  };
+};
+
+// Auto-canceling timer handle that takes a function object (e.g.: lambda) as
+// callback.
+class AutoCancelingTimerHandleWithFunction : public TimerHandleWithFunction
+{
+public:
+  AutoCancelingTimerHandleWithFunction() = default;
+  AutoCancelingTimerHandleWithFunction(const AutoCancelingTimerHandleWithFunction&) = default;
+  AutoCancelingTimerHandleWithFunction& operator=(const AutoCancelingTimerHandleWithFunction&) = default;
+  AutoCancelingTimerHandleWithFunction(AutoCancelingTimerHandleWithFunction&&) = default;
+  AutoCancelingTimerHandleWithFunction& operator=(AutoCancelingTimerHandleWithFunction&&) = default;
+  ~AutoCancelingTimerHandleWithFunction() { Cancel(); }
+};
+
+#endif // TimerUtils_h___
--- a/xpcom/glue/moz.build
+++ b/xpcom/glue/moz.build
@@ -75,16 +75,17 @@ EXPORTS.mozilla += [
     'EnumeratedArrayCycleCollection.h',
     'FileUtils.h',
     'GenericFactory.h',
     'IntentionalCrash.h',
     'Monitor.h',
     'Mutex.h',
     'Observer.h',
     'ReentrantMonitor.h',
+    'TimerUtils.h',
 ]
 
 include('objs.mozbuild')
 
 UNIFIED_SOURCES += xpcom_gluens_src_cppsrcs
 UNIFIED_SOURCES += xpcom_glue_src_cppsrcs
 
 UNIFIED_SOURCES += [
--- a/xpcom/glue/objs.mozbuild
+++ b/xpcom/glue/objs.mozbuild
@@ -25,16 +25,17 @@ xpcom_glue_src_lcppsrcs = [
     'nsMemory.cpp',
     'nsQuickSort.cpp',
     'nsTArray.cpp',
     'nsThreadUtils.cpp',
     'nsTObserverArray.cpp',
     'nsVersionComparator.cpp',
     'nsWeakReference.cpp',
     'PLDHashTable.cpp',
+    'TimerUtils.cpp',
 ]
 
 xpcom_glue_src_cppsrcs = [
     '/xpcom/glue/%s' % s for s in xpcom_glue_src_lcppsrcs
 ]
 
 xpcom_gluens_src_lcppsrcs = [
     'BlockingResourceBase.cpp',
--- a/xpcom/tests/TestTimers.cpp
+++ b/xpcom/tests/TestTimers.cpp
@@ -2,16 +2,17 @@
 /* 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 "TestHarness.h"
 
 #include "nsIThread.h"
 #include "nsITimer.h"
+#include "mozilla/TimerUtils.h"
 
 #include "nsCOMPtr.h"
 #include "nsComponentManagerUtils.h"
 #include "nsServiceManagerUtils.h"
 #include "nsThreadUtils.h"
 #include "prinrval.h"
 #include "prmon.h"
 #include "prthread.h"
@@ -442,25 +443,147 @@ FuzzTestTimers()
                          "Timed out waiting for all timers to pop");
       PR_Sleep(PR_MillisecondsToInterval(10));
     }
   }
 
   return NS_OK;
 }
 
+nsresult
+TestTimerUtils_TimerHandleWithFunction()
+{
+  AutoCreateAndDestroyReentrantMonitor newMon;
+  NS_ENSURE_TRUE(newMon, NS_ERROR_OUT_OF_MEMORY);
+
+  AutoTestThread testThread;
+  NS_ENSURE_TRUE(testThread, NS_ERROR_OUT_OF_MEMORY);
+
+  TimerHandleWithFunction timer;
+  nsresult rv;
+  rv = timer.Ensure();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsIEventTarget* target = static_cast<nsIEventTarget*>(testThread);
+
+  rv = timer->SetTarget(target);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsIThread* notifiedThread = nullptr;
+
+  nsIThread** threadPtr = &notifiedThread;
+  ReentrantMonitor* monPtr = newMon;
+  bool cleanedUp = false;
+  rv =
+    timer.InitWithCleanup(
+      2000,
+      nsITimer::TYPE_ONE_SHOT,
+      [threadPtr, monPtr, &cleanedUp]
+        (TimerHandleWithFunction::CallbackSituation aSituation)
+      {
+        if (aSituation == TimerHandleWithFunction::TimerFired) {
+          MOZ_ASSERT(threadPtr, "Callback was not supposed to be called!");
+          nsCOMPtr<nsIThread> current(do_GetCurrentThread());
+
+          ReentrantMonitorAutoEnter mon(*monPtr);
+
+          MOZ_ASSERT(!*threadPtr, "Timer called back more than once!");
+          *threadPtr = current;
+
+          mon.Notify();
+        } else {
+          MOZ_ASSERT(aSituation == TimerHandleWithFunction::ResourceDestroyed);
+          cleanedUp = true;
+        }
+      });
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  ReentrantMonitorAutoEnter mon(*newMon);
+  while (!notifiedThread) {
+    mon.Wait();
+  }
+  NS_ENSURE_TRUE(notifiedThread == testThread, NS_ERROR_FAILURE);
+  NS_ENSURE_TRUE(cleanedUp, NS_ERROR_FAILURE);
+
+  return NS_OK;
+}
+
+nsresult
+TestTimerUtils_AutoCancelingTimerHandleWithFunction()
+{
+  AutoCreateAndDestroyReentrantMonitor newMon;
+  NS_ENSURE_TRUE(newMon, NS_ERROR_OUT_OF_MEMORY);
+
+  AutoTestThread testThread;
+  NS_ENSURE_TRUE(testThread, NS_ERROR_OUT_OF_MEMORY);
+
+  nsIThread* notifiedThread = nullptr;
+  bool cleanedUp = false;
+
+  {
+    AutoCancelingTimerHandleWithFunction timer;
+    nsresult rv;
+    rv = timer.Ensure();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsIEventTarget* target = static_cast<nsIEventTarget*>(testThread);
+
+    rv = timer->SetTarget(target);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsIThread** threadPtr = &notifiedThread;
+    ReentrantMonitor* monPtr = newMon;
+    rv =
+      timer.InitWithCleanup(
+        2000,
+        nsITimer::TYPE_ONE_SHOT,
+        [threadPtr, monPtr, &cleanedUp]
+          (TimerHandleWithFunction::CallbackSituation aSituation)
+        {
+          if (aSituation == TimerHandleWithFunction::TimerFired) {
+            MOZ_ASSERT(threadPtr, "Callback was not supposed to be called!");
+            nsCOMPtr<nsIThread> current(do_GetCurrentThread());
+
+            ReentrantMonitorAutoEnter mon(*monPtr);
+
+            MOZ_ASSERT(!*threadPtr, "Timer called back more than once!");
+            *threadPtr = current;
+
+            mon.Notify();
+          } else {
+            MOZ_ASSERT(aSituation == TimerHandleWithFunction::ResourceDestroyed);
+            cleanedUp = true;
+          }
+        });
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    NS_ENSURE_FALSE(notifiedThread == testThread, NS_ERROR_FAILURE);
+    NS_ENSURE_FALSE(cleanedUp, NS_ERROR_FAILURE);
+
+    // End of scope for 'AutoCancelingTimerHandleWithFunction timer',
+    // should destroy timer *before* it has fired!
+  }
+
+  NS_ENSURE_FALSE(notifiedThread == testThread, NS_ERROR_FAILURE);
+  NS_ENSURE_TRUE(cleanedUp, NS_ERROR_FAILURE);
+
+  return NS_OK;
+}
+
 int main(int argc, char** argv)
 {
   ScopedXPCOM xpcom("TestTimers");
   NS_ENSURE_FALSE(xpcom.failed(), 1);
 
   static TestFuncPtr testsToRun[] = {
     TestTargetedTimers,
     TestTimerWithStoppedTarget,
-    FuzzTestTimers
+    FuzzTestTimers,
+    TestTimerUtils_TimerHandleWithFunction,
+    TestTimerUtils_AutoCancelingTimerHandleWithFunction
   };
   static uint32_t testCount = sizeof(testsToRun) / sizeof(testsToRun[0]);
 
   for (uint32_t i = 0; i < testCount; i++) {
     nsresult rv = testsToRun[i]();
     NS_ENSURE_SUCCESS(rv, 1);
   }