Bug 1070842 - Introduce an API on AutoJSAPI to allow Gecko to handle exceptions without meddling from the JS engine. r=bz
authorBobby Holley <bobbyholley@gmail.com>
Mon, 29 Sep 2014 15:34:21 +0200
changeset 230916 c8b6d2fd42f05b865f3f8154cbdac8166e72fd84
parent 230915 e84e6017e656d440dcce915f6873d758dd18b664
child 230917 ed7e4dde009b2e29ed05b05f32592edba5016b19
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1070842
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1070842 - Introduce an API on AutoJSAPI to allow Gecko to handle exceptions without meddling from the JS engine. r=bz
dom/base/ScriptSettings.cpp
dom/base/ScriptSettings.h
--- a/dom/base/ScriptSettings.cpp
+++ b/dom/base/ScriptSettings.cpp
@@ -229,40 +229,92 @@ FindJSContext(nsIGlobalObject* aGlobalOb
   if (!cx) {
     cx = nsContentUtils::GetSafeJSContext();
   }
   return cx;
 }
 
 AutoJSAPI::AutoJSAPI()
   : mCx(nullptr)
+  , mOwnErrorReporting(false)
+  , mOldDontReportUncaught(false)
 {
 }
 
+AutoJSAPI::~AutoJSAPI()
+{
+  if (mOwnErrorReporting) {
+    MOZ_ASSERT(NS_IsMainThread(), "See corresponding assertion in TakeOwnershipOfErrorReporting()");
+    JS::ContextOptionsRef(cx()).setDontReportUncaught(mOldDontReportUncaught);
+
+    if (HasException()) {
+
+      // AutoJSAPI uses a JSAutoNullableCompartment, and may be in a null
+      // compartment when the destructor is called. However, the JS engine
+      // requires us to be in a compartment when we fetch the pending exception.
+      // In this case, we enter the privileged junk scope and don't dispatch any
+      // error events.
+      JS::Rooted<JSObject*> errorGlobal(cx(), JS::CurrentGlobalOrNull(cx()));
+      if (!errorGlobal)
+        errorGlobal = xpc::PrivilegedJunkScope();
+      JSAutoCompartment ac(cx(), errorGlobal);
+      nsCOMPtr<nsPIDOMWindow> win = xpc::WindowGlobalOrNull(errorGlobal);
+      const char *category = nsContentUtils::IsCallerChrome() ? "chrome javascript"
+                                                              : "content javascript";
+      JS::Rooted<JS::Value> exn(cx());
+      js::ErrorReport jsReport(cx());
+      if (StealException(&exn) && jsReport.init(cx(), exn)) {
+        nsRefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
+        xpcReport->Init(jsReport.report(), jsReport.message(), category,
+                        win ? win->WindowID() : 0);
+        if (win) {
+          DispatchScriptErrorEvent(win, JS_GetRuntime(cx()), xpcReport, exn);
+        } else {
+          xpcReport->LogToConsole();
+        }
+      } else {
+        NS_WARNING("OOMed while acquiring uncaught exception from JSAPI");
+      }
+    }
+  }
+
+  if (mOldErrorReporter.isSome()) {
+    JS_SetErrorReporter(JS_GetRuntime(cx()), mOldErrorReporter.value());
+  }
+}
+
 void
 AutoJSAPI::InitInternal(JSObject* aGlobal, JSContext* aCx, bool aIsMainThread)
 {
   MOZ_ASSERT(aCx);
   mCx = aCx;
   if (aIsMainThread) {
     // This Rooted<> is necessary only as long as AutoCxPusher::AutoCxPusher
     // can GC, which is only possible because XPCJSContextStack::Push calls
     // nsIPrincipal.Equals. Once that is removed, the Rooted<> will no longer
     // be necessary.
     JS::Rooted<JSObject*> global(JS_GetRuntime(aCx), aGlobal);
     mCxPusher.emplace(mCx);
     mAutoNullableCompartment.emplace(mCx, global);
   } else {
     mAutoNullableCompartment.emplace(mCx, aGlobal);
   }
+
+  if (aIsMainThread) {
+    JSRuntime* rt = JS_GetRuntime(aCx);
+    mOldErrorReporter.emplace(JS_GetErrorReporter(rt));
+    JS_SetErrorReporter(rt, xpc::SystemErrorReporter);
+  }
 }
 
 AutoJSAPI::AutoJSAPI(nsIGlobalObject* aGlobalObject,
                      bool aIsMainThread,
                      JSContext* aCx)
+  : mOwnErrorReporting(false)
+  , mOldDontReportUncaught(false)
 {
   MOZ_ASSERT(aGlobalObject);
   MOZ_ASSERT(aGlobalObject->GetGlobalJSObject(), "Must have a JS global");
   MOZ_ASSERT(aCx);
   MOZ_ASSERT_IF(aIsMainThread, NS_IsMainThread());
 
   InitInternal(aGlobalObject->GetGlobalJSObject(), aCx, aIsMainThread);
 }
@@ -347,16 +399,59 @@ AutoJSAPI::InitWithLegacyErrorReporting(
 }
 
 bool
 AutoJSAPI::InitWithLegacyErrorReporting(nsGlobalWindow* aWindow)
 {
   return InitWithLegacyErrorReporting(static_cast<nsIGlobalObject*>(aWindow));
 }
 
+// Even with dontReportUncaught, the JS engine still sends warning reports
+// to the JSErrorReporter as soon as they are generated. These go directly to
+// the console, so we can handle them easily here.
+//
+// Eventually, SpiderMonkey will have a special-purpose callback for warnings only.
+void
+WarningOnlyErrorReporter(JSContext* aCx, const char* aMessage, JSErrorReport* aRep)
+{
+  MOZ_ASSERT(JSREPORT_IS_WARNING(aRep->flags));
+  nsRefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
+  const char* category = nsContentUtils::IsCallerChrome() ? "chrome javascript"
+                                                          : "content javascript";
+  nsPIDOMWindow* win = xpc::WindowGlobalOrNull(JS::CurrentGlobalOrNull(aCx));
+  xpcReport->Init(aRep, aMessage, category, win ? win->WindowID() : 0);
+  xpcReport->LogToConsole();
+}
+
+void
+AutoJSAPI::TakeOwnershipOfErrorReporting()
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Can't own error reporting off-main-thread yet");
+  MOZ_ASSERT(!mOwnErrorReporting);
+  mOwnErrorReporting = true;
+
+  JSRuntime *rt = JS_GetRuntime(cx());
+  mOldDontReportUncaught = JS::ContextOptionsRef(cx()).dontReportUncaught();
+  JS::ContextOptionsRef(cx()).setDontReportUncaught(true);
+  JS_SetErrorReporter(rt, WarningOnlyErrorReporter);
+}
+
+bool
+AutoJSAPI::StealException(JS::MutableHandle<JS::Value> aVal)
+{
+    MOZ_ASSERT(CxPusherIsStackTop());
+    MOZ_ASSERT(HasException());
+    MOZ_ASSERT(js::GetContextCompartment(cx()));
+    if (!JS_GetPendingException(cx(), aVal)) {
+      return false;
+    }
+    JS_ClearPendingException(cx());
+    return true;
+}
+
 AutoEntryScript::AutoEntryScript(nsIGlobalObject* aGlobalObject,
                                  bool aIsMainThread,
                                  JSContext* aCx)
   : AutoJSAPI(aGlobalObject, aIsMainThread,
               aCx ? aCx : FindJSContext(aGlobalObject))
   , ScriptSettingsStackEntry(aGlobalObject, /* aCandidate = */ true)
   , mWebIDLCallerPrincipal(nullptr)
 {
--- a/dom/base/ScriptSettings.h
+++ b/dom/base/ScriptSettings.h
@@ -198,16 +198,18 @@ private:
  * execution at inopportune moments via surreptitious getters and proxies.
  */
 class MOZ_STACK_CLASS AutoJSAPI {
 public:
   // Trivial constructor. One of the Init functions must be called before
   // accessing the JSContext through cx().
   AutoJSAPI();
 
+  ~AutoJSAPI();
+
   // This uses the SafeJSContext (or worker equivalent), and enters a null
   // compartment, so that the consumer is forced to select a compartment to
   // enter before manipulating objects.
   void Init();
 
   // This uses the SafeJSContext (or worker equivalent), and enters the
   // compartment of aGlobalObject.
   // If aGlobalObject or its associated JS global are null then it returns
@@ -248,29 +250,59 @@ public:
   JSContext* cx() const {
     MOZ_ASSERT(mCx, "Must call Init before using an AutoJSAPI");
     MOZ_ASSERT_IF(NS_IsMainThread(), CxPusherIsStackTop());
     return mCx;
   }
 
   bool CxPusherIsStackTop() const { return mCxPusher->IsStackTop(); }
 
+  // We're moving towards a world where the AutoJSAPI always handles
+  // exceptions that bubble up from the JS engine. In order to make this
+  // process incremental, we allow consumers to opt-in to the new behavior
+  // while keeping the old behavior as the default.
+  void TakeOwnershipOfErrorReporting();
+  bool OwnsErrorReporting() { return mOwnErrorReporting; }
+
+  bool HasException() const {
+    MOZ_ASSERT(CxPusherIsStackTop());
+    return JS_IsExceptionPending(cx());
+  };
+
+  // Transfers ownership of the current exception from the JS engine to the
+  // caller. Callers must ensure that HasException() is true, and that cx()
+  // is in a non-null compartment.
+  //
+  // Note that this fails if and only if we OOM while wrapping the exception
+  // into the current compartment.
+  bool StealException(JS::MutableHandle<JS::Value> aVal);
+
+  void ClearException() {
+    MOZ_ASSERT(CxPusherIsStackTop());
+    JS_ClearPendingException(cx());
+  }
+
 protected:
   // Protected constructor, allowing subclasses to specify a particular cx to
   // be used. This constructor initialises the AutoJSAPI, so Init must NOT be
   // called on subclasses that use this.
   // If aGlobalObject, its associated JS global or aCx are null this will cause
   // an assertion, as will setting aIsMainThread incorrectly.
   AutoJSAPI(nsIGlobalObject* aGlobalObject, bool aIsMainThread, JSContext* aCx);
 
 private:
   mozilla::Maybe<danger::AutoCxPusher> mCxPusher;
   mozilla::Maybe<JSAutoNullableCompartment> mAutoNullableCompartment;
   JSContext *mCx;
 
+  // Track state between the old and new error reporting modes.
+  bool mOwnErrorReporting;
+  bool mOldDontReportUncaught;
+  Maybe<JSErrorReporter> mOldErrorReporter;
+
   void InitInternal(JSObject* aGlobal, JSContext* aCx, bool aIsMainThread);
 };
 
 /*
  * A class that represents a new script entry point.
  */
 class AutoEntryScript : public AutoJSAPI,
                         protected ScriptSettingsStackEntry {