Bug 1293656 - Send crash pings for content crashes complete with stack traces r=bsmedberg
☠☠ backed out by 93d785448a69 ☠ ☠
authorGabriele Svelto <gsvelto@mozilla.com>
Wed, 19 Oct 2016 12:51:29 +0200
changeset 325294 c8545ffbd0cb18f7ae3a5810f5b98ec0ba8be0e0
parent 325293 f59861923f8169c003f0f7f8bd50e3fae52e0b51
child 325295 d2ba2e62f871b632535cf8de373f26da1fe649c1
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewersbsmedberg
bugs1293656
milestone53.0a1
Bug 1293656 - Send crash pings for content crashes complete with stack traces r=bsmedberg
ipc/glue/CrashReporterHost.cpp
ipc/glue/CrashReporterHost.h
toolkit/components/crashes/CrashManager.jsm
toolkit/components/crashes/CrashService.js
toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
toolkit/crashreporter/docs/index.rst
toolkit/crashreporter/nsExceptionHandler.cpp
toolkit/crashreporter/nsExceptionHandler.h
--- a/ipc/glue/CrashReporterHost.cpp
+++ b/ipc/glue/CrashReporterHost.cpp
@@ -1,20 +1,22 @@
 /* -*- 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 "CrashReporterHost.h"
 #include "CrashReporterMetadataShmem.h"
+#include "mozilla/LazyIdleThread.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/SyncRunnable.h"
 #include "mozilla/Telemetry.h"
 #ifdef MOZ_CRASHREPORTER
+# include "nsIAsyncShutdown.h"
 # include "nsICrashService.h"
 #endif
 
 namespace mozilla {
 namespace ipc {
 
 CrashReporterHost::CrashReporterHost(GeckoProcessType aProcessType,
                                      const Shmem& aShmem)
@@ -58,38 +60,173 @@ CrashReporterHost::GenerateCrashReport(R
   notes.Put(NS_LITERAL_CSTRING("StartupTime"), nsDependentCString(startTime));
 
   CrashReporterMetadataShmem::ReadAppNotes(mShmem, &notes);
 
   CrashReporter::AppendExtraData(dumpID, notes);
   NotifyCrashService(mProcessType, dumpID, &notes);
 }
 
+/**
+ * Runnable used to execute the minidump analyzer program asynchronously after
+ * a crash. This should run on a background thread not to block the main thread
+ * over the potentially long minidump analysis. Once analysis is over, the
+ * crash information will be relayed to the crash manager via another runnable
+ * sent to the main thread. Shutdown will be blocked for the duration of the
+ * entire process to ensure this information is sent.
+ */
+class AsyncMinidumpAnalyzer final : public nsIRunnable
+                                  , public nsIAsyncShutdownBlocker
+{
+public:
+  /**
+   * Create a new minidump analyzer runnable, this will also block shutdown
+   * until the associated crash has been added to the crash manager.
+   */
+  AsyncMinidumpAnalyzer(int32_t aProcessType,
+                        int32_t aCrashType,
+                        const nsString& aChildDumpID)
+    : mProcessType(aProcessType)
+    , mCrashType(aCrashType)
+    , mChildDumpID(aChildDumpID)
+    , mName(NS_LITERAL_STRING("Crash reporter: blocking on minidump analysis"))
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsresult rv = GetShutdownBarrier()->AddBlocker(
+      this, NS_LITERAL_STRING(__FILE__), __LINE__,
+      NS_LITERAL_STRING("Minidump analysis"));
+    MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+  }
+
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+
+    if (mProcessType == nsICrashService::PROCESS_TYPE_CONTENT) {
+      CrashReporter::RunMinidumpAnalyzer(mChildDumpID);
+    }
+
+    // Make a copy of these so we can copy them into the runnable lambda
+    int32_t processType = mProcessType;
+    int32_t crashType = mCrashType;
+    nsString childDumpID(mChildDumpID);
+
+    NS_DispatchToMainThread(NS_NewRunnableFunction([=] () -> void {
+      nsCOMPtr<nsICrashService> crashService =
+        do_GetService("@mozilla.org/crashservice;1");
+      if (crashService) {
+        crashService->AddCrash(processType, crashType, childDumpID);
+      }
+
+      nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+
+      if (barrier) {
+        barrier->RemoveBlocker(this);
+      }
+    }));
+
+    return NS_OK;
+  }
+
+  NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aBarrierClient) override
+  {
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetName(nsAString& aName) override
+  {
+    aName = mName;
+    return NS_OK;
+  }
+
+  NS_IMETHOD GetState(nsIPropertyBag**) override
+  {
+    return NS_OK;
+  }
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+private:
+  ~AsyncMinidumpAnalyzer() {}
+
+  // Returns the profile-before-change shutdown barrier
+  static nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
+    nsCOMPtr<nsIAsyncShutdownClient> barrier;
+    nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier));
+
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return nullptr;
+    }
+
+    return barrier.forget();
+  }
+
+  int32_t mProcessType;
+  int32_t mCrashType;
+  const nsString mChildDumpID;
+  const nsString mName;
+};
+
+NS_IMPL_ISUPPORTS(AsyncMinidumpAnalyzer, nsIRunnable, nsIAsyncShutdownBlocker)
+
+/**
+ * Add information about a crash to the crash manager. This method runs the
+ * various activities required to gather additional information and notify the
+ * crash manager asynchronously, since many of them involve I/O or potentially
+ * long processing.
+ *
+ * @param aProcessType The type of process that crashed
+ * @param aCrashType The type of crash (crash or hang)
+ * @param aChildDumpID A string holding the ID of the dump associated with this
+ *        crash
+ */
+/* static */ void
+CrashReporterHost::AsyncAddCrash(int32_t aProcessType,
+                                 int32_t aCrashType,
+                                 const nsString& aChildDumpID)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  static StaticRefPtr<LazyIdleThread> sBackgroundThread;
+
+  if (!sBackgroundThread) {
+    // Background thread used for running minidump analysis. It will be
+    // destroyed immediately after it's done with the task.
+    sBackgroundThread =
+      new LazyIdleThread(0, NS_LITERAL_CSTRING("CrashReporterHost"));
+    ClearOnShutdown(&sBackgroundThread);
+  }
+
+  RefPtr<AsyncMinidumpAnalyzer> task =
+    new AsyncMinidumpAnalyzer(aProcessType, aCrashType, aChildDumpID);
+
+  Unused << sBackgroundThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
 /* static */ void
 CrashReporterHost::NotifyCrashService(GeckoProcessType aProcessType,
                                       const nsString& aChildDumpID,
                                       const AnnotationTable* aNotes)
 {
   if (!NS_IsMainThread()) {
     RefPtr<Runnable> runnable = NS_NewRunnableFunction([=] () -> void {
       CrashReporterHost::NotifyCrashService(aProcessType, aChildDumpID, aNotes);
     });
     RefPtr<nsIThread> mainThread = do_GetMainThread();
     SyncRunnable::DispatchToThread(mainThread, runnable);
     return;
   }
 
   MOZ_ASSERT(!aChildDumpID.IsEmpty());
 
-  nsCOMPtr<nsICrashService> crashService =
-    do_GetService("@mozilla.org/crashservice;1");
-  if (!crashService) {
-    return;
-  }
-
   int32_t processType;
   int32_t crashType = nsICrashService::CRASH_TYPE_CRASH;
 
   nsCString telemetryKey;
 
   switch (aProcessType) {
     case GeckoProcessType_Content:
       processType = nsICrashService::PROCESS_TYPE_CONTENT;
@@ -114,15 +251,15 @@ CrashReporterHost::NotifyCrashService(Ge
       processType = nsICrashService::PROCESS_TYPE_GPU;
       telemetryKey.AssignLiteral("gpu");
       break;
     default:
       NS_ERROR("unknown process type");
       return;
   }
 
-  crashService->AddCrash(processType, crashType, aChildDumpID);
+  AsyncAddCrash(processType, crashType, aChildDumpID);
   Telemetry::Accumulate(Telemetry::SUBPROCESS_CRASHES_WITH_DUMP, telemetryKey, 1);
 }
 #endif
 
 } // namespace ipc
 } // namespace mozilla
--- a/ipc/glue/CrashReporterHost.h
+++ b/ipc/glue/CrashReporterHost.h
@@ -45,16 +45,18 @@ public:
   static void NotifyCrashService(
     GeckoProcessType aProcessType,
     const nsString& aChildDumpID,
     const AnnotationTable* aNotes);
 #endif
 
 private:
   void GenerateCrashReport(RefPtr<nsIFile> aCrashDump);
+  static void AsyncAddCrash(int32_t aProcessType, int32_t aCrashType,
+                            const nsString& aChildDumpID);
 
 private:
   GeckoProcessType mProcessType;
   Shmem mShmem;
   time_t mStartTime;
 };
 
 } // namespace ipc
--- a/toolkit/components/crashes/CrashManager.jsm
+++ b/toolkit/components/crashes/CrashManager.jsm
@@ -139,16 +139,19 @@ this.CrashManager = function(options) {
 
   // Promise for in-progress aggregation operation. We store it on the
   // object so it can be returned for in-progress operations.
   this._aggregatePromise = null;
 
   // Map of crash ID / promise tuples used to track adding new crashes.
   this._crashPromises = new Map();
 
+  // Promise for the crash ping used only for testing.
+  this._pingPromise = null;
+
   // The CrashStore currently attached to this object.
   this._store = null;
 
   // A Task to retrieve the store. This is needed to avoid races when
   // _getStore() is called multiple times in a short interval.
   this._getStoreTask = null;
 
   // The timer controlling the expiration of the CrashStore instance.
@@ -444,17 +447,22 @@ this.CrashManager.prototype = Object.fre
       }
 
       let deferred = this._crashPromises.get(id);
 
       if (deferred) {
         this._crashPromises.delete(id);
         deferred.resolve();
       }
-    }.bind(this));
+
+      // Send a telemetry ping for each content process crash
+      if (processType === this.PROCESS_TYPE_CONTENT) {
+        this._sendCrashPing(id, processType, date, metadata);
+      }
+   }.bind(this));
 
     return promise;
   },
 
   /**
    * Returns a promise that is resolved only the crash with the specified id
    * has been fully recorded.
    *
@@ -604,42 +612,42 @@ this.CrashManager.prototype = Object.fre
       }
       let date = new Date(time * 1000);
       let payload = data.substring(start);
 
       return this._handleEventFilePayload(store, entry, type, date, payload);
     }.bind(this));
   },
 
-  _filterAnnotations: function (annotations) {
+  _filterAnnotations: function(annotations) {
     let filteredAnnotations = {};
 
     for (let line in annotations) {
       if (this.ANNOTATION_WHITELIST.includes(line)) {
         filteredAnnotations[line] = annotations[line];
       }
     }
 
     return filteredAnnotations;
   },
 
-  _sendCrashPing: function (crashId, type, date, metadata) {
+  _sendCrashPing: function(crashId, type, date, metadata = {}) {
     // If we have a saved environment, use it. Otherwise report
     // the current environment.
     let reportMeta = Cu.cloneInto(metadata, myScope);
     let crashEnvironment = parseAndRemoveField(reportMeta,
                                                "TelemetryEnvironment");
     let sessionId = parseAndRemoveField(reportMeta, "TelemetrySessionId",
                                         /* parseAsJson */ false);
     let stackTraces = parseAndRemoveField(reportMeta, "StackTraces");
 
     // Filter the remaining annotations to remove privacy-sensitive ones
     reportMeta = this._filterAnnotations(reportMeta);
 
-    TelemetryController.submitExternalPing("crash",
+    this._pingPromise = TelemetryController.submitExternalPing("crash",
       {
         version: 1,
         crashDate: date.toISOString().slice(0, 10), // YYYY-MM-DD
         sessionId: sessionId,
         crashId: crashId,
         processType: type,
         stackTraces: stackTraces,
         metadata: reportMeta,
--- a/toolkit/components/crashes/CrashService.js
+++ b/toolkit/components/crashes/CrashService.js
@@ -1,20 +1,55 @@
 /* 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/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
+Cu.import("resource://gre/modules/KeyValueParser.jsm");
+Cu.import("resource://gre/modules/osfile.jsm", this);
+Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 
 /**
+ * Process the .extra file associated with the crash id and return the
+ * annotations it contains in an object.
+ *
+ * @param crashID {string} Crash ID. Likely a UUID.
+ *
+ * @return {Promise} A promise that resolves to an object holding the crash
+ *         annotations, this object may be empty if no annotations were found.
+ */
+function processExtraFile(id) {
+  let crDir = OS.Path.join(OS.Constants.Path.userApplicationDataDir,
+                           "Crash Reports");
+  let pendingDumpsDir = OS.Path.join(crDir, "pending");
+  let extraPath = OS.Path.join(pendingDumpsDir, id + ".extra");
+
+  return Task.spawn(function* () {
+    try {
+      let decoder = new TextDecoder();
+      let extraFile = yield OS.File.read(extraPath);
+      let extraData = decoder.decode(extraFile);
+
+      return parseKeyValuePairs(extraData);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+
+    return {};
+  });
+}
+
+/**
  * This component makes crash data available throughout the application.
  *
  * It is a service because some background activity will eventually occur.
  */
 this.CrashService = function() {};
 
 CrashService.prototype = Object.freeze({
   classID: Components.ID("{92668367-1b17-4190-86b2-1061b2179744}"),
@@ -50,17 +85,23 @@ CrashService.prototype = Object.freeze({
       break;
     case Ci.nsICrashService.CRASH_TYPE_HANG:
       crashType = Services.crashmanager.CRASH_TYPE_HANG;
       break;
     default:
       throw new Error("Unrecognized CRASH_TYPE: " + crashType);
     }
 
-    Services.crashmanager.addCrash(processType, crashType, id, new Date());
+    AsyncShutdown.profileBeforeChange.addBlocker(
+      "CrashService waiting for content crash ping to be sent",
+      processExtraFile(id).then(metadata => {
+        return Services.crashmanager.addCrash(processType, crashType, id,
+                                              new Date(), metadata)
+      })
+    );
   },
 
   observe: function(subject, topic, data) {
     switch (topic) {
       case "profile-after-change":
         // Side-effect is the singleton is instantiated.
         Services.crashmanager;
         break;
--- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
@@ -207,35 +207,35 @@ add_task(function* test_schedule_mainten
   let crashes = yield m.getCrashes();
   Assert.equal(crashes.length, 1);
   Assert.equal(crashes[0].id, "id1");
 });
 
 const crashId = "3cb67eba-0dc7-6f78-6a569a0e-172287ec";
 const productName = "Firefox";
 const productId = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+const stackTraces = "{\"status\":\"OK\"}";
 
 add_task(function* test_main_crash_event_file() {
   let ac = new TelemetryArchiveTesting.Checker();
   yield ac.promiseInit();
   let theEnvironment = TelemetryEnvironment.currentEnvironment;
   const sessionId = "be66af2f-2ee5-4330-ae95-44462dfbdf0c";
-  let stackTraces = { status: "OK" };
 
   // To test proper escaping, add data to the environment with an embedded
   // double-quote
   theEnvironment.testValue = "MyValue\"";
 
   let m = yield getManager();
   const fileContent = crashId + "\n" +
     "ProductName=" + productName + "\n" +
     "ProductID=" + productId + "\n" +
     "TelemetryEnvironment=" + JSON.stringify(theEnvironment) + "\n" +
     "TelemetrySessionId=" + sessionId + "\n" +
-    "StackTraces=" + JSON.stringify(stackTraces) + "\n" +
+    "StackTraces=" + stackTraces + "\n" +
     "ThisShouldNot=end-up-in-the-ping\n";
 
   yield m.createEventsFile(crashId, "crash.main.2", DUMMY_DATE, fileContent);
   let count = yield m.aggregateEventsFiles();
   Assert.equal(count, 1);
 
   let crashes = yield m.getCrashes();
   Assert.equal(crashes.length, 1);
@@ -453,16 +453,38 @@ add_task(function* test_addCrash() {
 
   crash = map.get("changing-item");
   Assert.ok(!!crash);
   Assert.equal(crash.crashDate, DUMMY_DATE_2);
   Assert.equal(crash.type, m.PROCESS_TYPE_CONTENT + "-" + m.CRASH_TYPE_HANG);
   Assert.ok(crash.isOfType(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_HANG));
 });
 
+add_task(function* test_content_crash_ping() {
+  let ac = new TelemetryArchiveTesting.Checker();
+  yield ac.promiseInit();
+
+  let m = yield getManager();
+  let id = yield m.createDummyDump();
+  yield m.addCrash(m.PROCESS_TYPE_CONTENT, m.CRASH_TYPE_CRASH, id, DUMMY_DATE, {
+    StackTraces: stackTraces,
+    ThisShouldNot: "end-up-in-the-ping"
+  });
+  yield m._pingPromise;
+
+  let found = yield ac.promiseFindPing("crash", [
+    [["payload", "crashId"], id],
+    [["payload", "processType"], m.PROCESS_TYPE_CONTENT],
+    [["payload", "stackTraces", "status"], "OK"],
+  ]);
+  Assert.ok(found, "Telemetry ping submitted for content crash");
+  Assert.equal(found.payload.metadata.ThisShouldNot, undefined,
+               "Non-whitelisted fields should be filtered out");
+});
+
 add_task(function* test_generateSubmissionID() {
   let m = yield getManager();
 
   const SUBMISSION_ID_REGEX =
     /^(sub-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
   let id = m.generateSubmissionID();
   Assert.ok(SUBMISSION_ID_REGEX.test(id));
 });
--- a/toolkit/crashreporter/docs/index.rst
+++ b/toolkit/crashreporter/docs/index.rst
@@ -125,16 +125,25 @@ actor should check for it by calling Tak
 Method: see ``mozilla::plugins::PluginModuleParent::ActorDestroy`` and
 ``mozilla::plugins::PluginModuleParent::ProcessFirstMinidump``. That method
 is responsible for calling
 ``mozilla::dom::CrashReporterParent::GenerateCrashReportForMinidump`` with
 appropriate crash annotations specific to the crash. All child-process
 crashes are annotated with a ``ProcessType`` annotation, such as "content" or
 "plugin".
 
+Once the minidump file has been generated the
+``mozilla::dom::CrashReporterHost`` is notified of the crash. It will first
+try to extract the stack traces from the minidump file using the
+*minidump analyzer*. Then the stack traces will be stored in the extra file
+together with the rest of the crash annotations and finally the crash will be
+recorded by calling ```CrashService.addCrash()```. This last step adds the
+crash to the ```CrashManager``` database and automatically sends a crash ping
+with information about the crash.
+
 Submission of child process crashes is handled by application code. This
 code prompts the user to submit crashes in context-appropriate UI and then
 submits the crashes using ``CrashSubmit.jsm``.
 
 Memory Reports
 ==============
 
 When a process detects that it is running low on memory, a memory report is
--- a/toolkit/crashreporter/nsExceptionHandler.cpp
+++ b/toolkit/crashreporter/nsExceptionHandler.cpp
@@ -3,16 +3,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 "nsExceptionHandler.h"
 
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryService.h"
 #include "nsDataHashtable.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/dom/CrashReporterChild.h"
 #include "mozilla/ipc/CrashReporterClient.h"
 #include "mozilla/Services.h"
 #include "nsIObserverService.h"
 #include "mozilla/Unused.h"
 #include "mozilla/Sprintf.h"
@@ -124,16 +125,17 @@ namespace CrashReporter {
 #ifdef XP_WIN32
 typedef wchar_t XP_CHAR;
 typedef std::wstring xpstring;
 #define XP_TEXT(x) L##x
 #define CONVERT_XP_CHAR_TO_UTF16(x) x
 #define XP_STRLEN(x) wcslen(x)
 #define my_strlen strlen
 #define CRASH_REPORTER_FILENAME "crashreporter.exe"
+#define MINIDUMP_ANALYZER_FILENAME "minidump-analyzer.exe"
 #define PATH_SEPARATOR "\\"
 #define XP_PATH_SEPARATOR L"\\"
 #define XP_PATH_SEPARATOR_CHAR L'\\'
 #define XP_PATH_MAX (MAX_PATH + 1)
 // "<reporter path>" "<minidump path>"
 #define CMDLINE_SIZE ((XP_PATH_MAX * 2) + 6)
 #ifdef _USE_32BIT_TIME_T
 #define XP_TTOA(time, buffer, base) ltoa(time, buffer, base)
@@ -142,16 +144,17 @@ typedef std::wstring xpstring;
 #endif
 #define XP_STOA(size, buffer, base) _ui64toa(size, buffer, base)
 #else
 typedef char XP_CHAR;
 typedef std::string xpstring;
 #define XP_TEXT(x) x
 #define CONVERT_XP_CHAR_TO_UTF16(x) NS_ConvertUTF8toUTF16(x)
 #define CRASH_REPORTER_FILENAME "crashreporter"
+#define MINIDUMP_ANALYZER_FILENAME "minidump-analyzer"
 #define PATH_SEPARATOR "/"
 #define XP_PATH_SEPARATOR "/"
 #define XP_PATH_SEPARATOR_CHAR '/'
 #define XP_PATH_MAX PATH_MAX
 #ifdef XP_LINUX
 #define XP_STRLEN(x) my_strlen(x)
 #define XP_TTOA(time, buffer, base) my_inttostring(time, buffer, sizeof(buffer))
 #define XP_STOA(size, buffer, base) my_inttostring(size, buffer, sizeof(buffer))
@@ -185,16 +188,19 @@ static xpstring *defaultMemoryReportPath
 
 static const char kCrashMainID[] = "crash.main.2\n";
 
 static google_breakpad::ExceptionHandler* gExceptionHandler = nullptr;
 
 static XP_CHAR* pendingDirectory;
 static XP_CHAR* crashReporterPath;
 static XP_CHAR* memoryReportPath;
+#if !defined(MOZ_WIDGET_ANDROID)
+static XP_CHAR* minidumpAnalyzerPath;
+#endif // !defined(MOZ_WIDGET_ANDROID)
 
 // Where crash events should go.
 static XP_CHAR* eventsDirectory;
 static char* eventsEnv = nullptr;
 
 // The current telemetry session ID to write to the event file
 static char* currentSessionId = nullptr;
 
@@ -1531,16 +1537,42 @@ ChildFilter(void* context)
 {
   bool result = Filter(context);
   if (result) {
     PrepareChildExceptionTimeAnnotations();
   }
   return result;
 }
 
+#if !defined(MOZ_WIDGET_ANDROID)
+
+// Locate the specified executable and store its path as a native string in
+// the |aPathPtr| so we can later invoke it from within the exception handler.
+static nsresult
+LocateExecutable(nsIFile* aXREDirectory, const nsACString& aName,
+                 nsAString& aPath)
+{
+  nsCOMPtr<nsIFile> exePath;
+  nsresult rv = aXREDirectory->Clone(getter_AddRefs(exePath));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef XP_MACOSX
+  exePath->SetNativeLeafName(NS_LITERAL_CSTRING("MacOS"));
+  exePath->Append(NS_LITERAL_STRING("crashreporter.app"));
+  exePath->Append(NS_LITERAL_STRING("Contents"));
+  exePath->Append(NS_LITERAL_STRING("MacOS"));
+#endif
+
+  exePath->AppendNative(aName);
+  exePath->GetPath(aPath);
+  return NS_OK;
+}
+
+#endif // !defined(MOZ_WIDGET_ANDROID)
+
 nsresult SetExceptionHandler(nsIFile* aXREDirectory,
                              bool force/*=false*/)
 {
   if (gExceptionHandler)
     return NS_ERROR_ALREADY_INITIALIZED;
 
 #if !defined(DEBUG) || defined(MOZ_WIDGET_GONK)
   // In non-debug builds, enable the crash reporter by default, and allow
@@ -1581,54 +1613,57 @@ nsresult SetExceptionHandler(nsIFile* aX
   crashReporterAPIData_Hash =
     new nsDataHashtable<nsCStringHashKey,nsCString>();
   NS_ENSURE_TRUE(crashReporterAPIData_Hash, NS_ERROR_OUT_OF_MEMORY);
 
   notesField = new nsCString();
   NS_ENSURE_TRUE(notesField, NS_ERROR_OUT_OF_MEMORY);
 
   if (!headlessClient) {
-    // locate crashreporter executable
-    nsCOMPtr<nsIFile> exePath;
-    nsresult rv = aXREDirectory->Clone(getter_AddRefs(exePath));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-#if defined(XP_MACOSX)
-    exePath->SetNativeLeafName(NS_LITERAL_CSTRING("MacOS"));
-    exePath->Append(NS_LITERAL_STRING("crashreporter.app"));
-    exePath->Append(NS_LITERAL_STRING("Contents"));
-    exePath->Append(NS_LITERAL_STRING("MacOS"));
-#endif
-
-    exePath->AppendNative(NS_LITERAL_CSTRING(CRASH_REPORTER_FILENAME));
+#if !defined(MOZ_WIDGET_ANDROID)
+    // Locate the crash reporter executable
+    nsAutoString crashReporterPath_temp;
+    nsresult rv = LocateExecutable(aXREDirectory,
+                                   NS_LITERAL_CSTRING(CRASH_REPORTER_FILENAME),
+                                   crashReporterPath_temp);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsAutoString minidumpAnalyzerPath_temp;
+    rv = LocateExecutable(aXREDirectory,
+                          NS_LITERAL_CSTRING(MINIDUMP_ANALYZER_FILENAME),
+                          minidumpAnalyzerPath_temp);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
 
 #ifdef XP_WIN32
-    nsString crashReporterPath_temp;
-
-    exePath->GetPath(crashReporterPath_temp);
-    crashReporterPath = reinterpret_cast<wchar_t*>(ToNewUnicode(crashReporterPath_temp));
-#elif !defined(__ANDROID__)
-    nsCString crashReporterPath_temp;
-
-    exePath->GetNativePath(crashReporterPath_temp);
-    crashReporterPath = ToNewCString(crashReporterPath_temp);
+  crashReporterPath =
+    reinterpret_cast<wchar_t*>(ToNewUnicode(crashReporterPath_temp));
+  minidumpAnalyzerPath =
+    reinterpret_cast<wchar_t*>(ToNewUnicode(minidumpAnalyzerPath_temp));
+#else
+  crashReporterPath = ToNewCString(crashReporterPath_temp);
+  minidumpAnalyzerPath = ToNewCString(minidumpAnalyzerPath_temp);
+#endif // XP_WIN32
 #else
     // On Android, we launch using the application package name instead of a
     // filename, so use the dynamically set MOZ_ANDROID_PACKAGE_NAME, or fall
     // back to the static ANDROID_PACKAGE_NAME.
     const char* androidPackageName = PR_GetEnv("MOZ_ANDROID_PACKAGE_NAME");
     if (androidPackageName != nullptr) {
       nsCString package(androidPackageName);
       package.Append("/org.mozilla.gecko.CrashReporter");
       crashReporterPath = ToNewCString(package);
     } else {
       nsCString package(ANDROID_PACKAGE_NAME "/org.mozilla.gecko.CrashReporter");
       crashReporterPath = ToNewCString(package);
     }
-#endif
+#endif // !defined(MOZ_WIDGET_ANDROID)
   }
 
   // get temp path to use for minidump path
 #if defined(XP_WIN32)
   nsString tempPath;
 #else
   nsCString tempPath;
 #endif
@@ -3028,16 +3063,40 @@ bool
 AppendExtraData(const nsAString& id, const AnnotationTable& data)
 {
   nsCOMPtr<nsIFile> extraFile;
   if (!GetExtraFileForID(id, getter_AddRefs(extraFile)))
     return false;
   return AppendExtraData(extraFile, data);
 }
 
+/**
+ * Runs the minidump analyzer program on the specified crash dump. The analyzer
+ * will extract the stack traces from the dump and store them in JSON format as
+ * an annotation in the extra file associated with the crash.
+ *
+ * This method waits synchronously for the program to have finished executing,
+ * do not call it from the main thread!
+ */
+void
+RunMinidumpAnalyzer(const nsAString& id)
+{
+#if !defined(MOZ_WIDGET_ANDROID)
+  nsCOMPtr<nsIFile> file;
+
+  if (CrashReporter::GetMinidumpForID(id, getter_AddRefs(file)) && file) {
+    nsAutoString path;
+
+    file->GetPath(path);
+    LaunchProgram(minidumpAnalyzerPath,
+                  (XP_CHAR*)(NS_ConvertUTF16toUTF8(path).get()));
+  }
+#endif // !defined(MOZ_WIDGET_ANDROID)
+}
+
 //-----------------------------------------------------------------------------
 // Helpers for AppendExtraData()
 //
 struct Blacklist {
   Blacklist() : mItems(nullptr), mLen(0) { }
   Blacklist(const char** items, int len) : mItems(items), mLen(len) { }
 
   bool Contains(const nsACString& key) const {
--- a/toolkit/crashreporter/nsExceptionHandler.h
+++ b/toolkit/crashreporter/nsExceptionHandler.h
@@ -98,16 +98,17 @@ typedef nsDataHashtable<nsCStringHashKey
 
 void DeleteMinidumpFilesForID(const nsAString& id);
 bool GetMinidumpForID(const nsAString& id, nsIFile** minidump);
 bool GetIDFromMinidump(nsIFile* minidump, nsAString& id);
 bool GetExtraFileForID(const nsAString& id, nsIFile** extraFile);
 bool GetExtraFileForMinidump(nsIFile* minidump, nsIFile** extraFile);
 bool AppendExtraData(const nsAString& id, const AnnotationTable& data);
 bool AppendExtraData(nsIFile* extraFile, const AnnotationTable& data);
+void RunMinidumpAnalyzer(const nsAString& id);
 
 /*
  * Renames the stand alone dump file aDumpFile to:
  *  |aOwnerDumpFile-aDumpFileProcessType.dmp|
  * and moves it into the same directory as aOwnerDumpFile. Does not
  * modify aOwnerDumpFile in any way.
  *
  * @param aDumpFile - the dump file to associate with aOwnerDumpFile.