Bug 1310703 - Make the crashreporter client send a crash ping via the ping sender executable; r?ted draft
authorGabriele Svelto <gsvelto@mozilla.com>
Mon, 16 Jan 2017 18:14:17 +0100
changeset 482804 630ea8a52f31e1e308f37f5c515be78a9066f5a0
parent 482803 9f8cf41a95a387a7b9cd67bf4d861cd1db0d698c
child 482805 148db521c40846dea123e88b093c92a5a7dcdb5b
push id45169
push usergsvelto@mozilla.com
push dateMon, 13 Feb 2017 13:46:14 +0000
reviewersted
bugs1310703
milestone54.0a1
Bug 1310703 - Make the crashreporter client send a crash ping via the ping sender executable; r?ted MozReview-Commit-ID: LKKpO94smLk
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/docs/data/crash-ping.rst
toolkit/crashreporter/client/crashreporter.cpp
toolkit/crashreporter/client/crashreporter.h
toolkit/crashreporter/client/crashreporter_unix_common.cpp
toolkit/crashreporter/client/crashreporter_win.cpp
toolkit/crashreporter/client/moz.build
toolkit/crashreporter/client/ping.cpp
toolkit/crashreporter/docs/index.rst
toolkit/crashreporter/nsExceptionHandler.cpp
toolkit/crashreporter/test/unit/test_crashreporter_crash.js
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -17,16 +17,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/DeferredTask.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
 Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
 Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/ClientID.jsm");
 
 const Utils = TelemetryUtils;
 
 const myScope = this;
 
 // When modifying the payload in incompatible ways, please bump this version number
 const PAYLOAD_VERSION = 4;
 const PING_TYPE_MAIN = "main";
@@ -61,17 +62,17 @@ const MIN_SUBSESSION_LENGTH_MS = Prefere
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetrySession" + (Utils.isContentProcess ? "#content::" : "::");
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit.enabled";
 const PREF_UNIFIED = PREF_BRANCH + "unified";
-
+const PREF_SERVER = PREF_BRANCH + "server";
 
 const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
 const MESSAGE_TELEMETRY_THREAD_HANGS = "Telemetry:ChildThreadHangs";
 const MESSAGE_TELEMETRY_GET_CHILD_THREAD_HANGS = "Telemetry:GetChildThreadHangs";
 const MESSAGE_TELEMETRY_USS = "Telemetry:USS";
 const MESSAGE_TELEMETRY_GET_CHILD_USS = "Telemetry:GetChildUSS";
 
 const DATAREPORTING_DIRECTORY = "datareporting";
@@ -198,22 +199,35 @@ function getPingType(aPayload) {
   }
 
   return PING_TYPE_MAIN;
 }
 
 /**
  * Annotate the current session ID with the crash reporter to map potential
  * crash pings with the related main ping.
+ *
+ * @param sessionId {String} The telemetry session ID
+ * @param clientId {String} The telemetry client ID
+ * @param telemetryServer {String} The URL of the telemetry server
  */
-function annotateCrashReport(sessionId) {
+function annotateCrashReport(sessionId, clientId, telemetryServer) {
   try {
     const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"];
     if (cr) {
-      cr.getService(Ci.nsICrashReporter).setTelemetrySessionId(sessionId);
+      const crs = cr.getService(Ci.nsICrashReporter);
+
+      crs.setTelemetrySessionId(sessionId);
+
+      // We do not annotate the crash if telemetry is disabled to prevent the
+      // crashreporter client from sending the crash ping.
+      if (Utils.isTelemetryEnabled) {
+        crs.annotateCrashReport("TelemetryClientId", clientId);
+        crs.annotateCrashReport("TelemetryServerURL", telemetryServer);
+      }
     }
   } catch (e) {
     // Ignore errors when crash reporting is disabled
   }
 }
 
 /**
  * Read current process I/O counters.
@@ -1466,17 +1480,20 @@ var Impl = {
     // Generate a unique id once per session so the server can cope with duplicate
     // submissions, orphaning and other oddities. The id is shared across subsessions.
     this._sessionId = Policy.generateSessionUUID();
     this.startNewSubsession();
     // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use
     // the very same value for |_sessionStartDate|.
     this._sessionStartDate = this._subsessionStartDate;
 
-    annotateCrashReport(this._sessionId);
+    // Annotate crash reports using the cached client ID which can be accessed
+    // synchronously. If it is not available yet, we'll update it later.
+    annotateCrashReport(this._sessionId, ClientID.getCachedClientID(),
+                        Preferences.get(PREF_SERVER, undefined));
 
     // Initialize some probes that are kept in their own modules
     this._thirdPartyCookies = new ThirdPartyCookieProbe();
     this._thirdPartyCookies.init();
 
     // Record old value and update build ID preference if this is the first
     // run with a new build ID.
     let previousBuildId = Preferences.get(PREF_PREVIOUS_BUILDID, null);
@@ -1493,23 +1510,23 @@ var Impl = {
     }
     Services.obs.addObserver(this, "xul-window-visible", false);
     this._hasWindowRestoredObserver = true;
     this._hasXulWindowVisibleObserver = true;
 
     ppml.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this);
     ppml.addMessageListener(MESSAGE_TELEMETRY_THREAD_HANGS, this);
     ppml.addMessageListener(MESSAGE_TELEMETRY_USS, this);
-},
+  },
 
-/**
-  * Does the "heavy" Telemetry initialization later on, so we
-  * don't impact startup performance.
-  * @return {Promise} Resolved when the initialization completes.
-  */
+  /**
+   * Does the "heavy" Telemetry initialization later on, so we
+   * don't impact startup performance.
+   * @return {Promise} Resolved when the initialization completes.
+   */
   delayedInit() {
     this._log.trace("delayedInit");
 
     this._delayedInitTask = Task.spawn(function* () {
       try {
         this._initialized = true;
 
         yield this._loadSessionData();
@@ -1521,16 +1538,20 @@ var Impl = {
         this.gatherMemory();
 
         if (Telemetry.canRecordExtended) {
           GCTelemetry.init();
         }
 
         Telemetry.asyncFetchTelemetryData(function() {});
 
+        // Update the crash annotation with the proper client ID.
+        annotateCrashReport(this._sessionId, yield ClientID.getClientID(),
+                            Preferences.get(PREF_SERVER, undefined));
+
         if (IS_UNIFIED_TELEMETRY) {
           // Check for a previously written aborted session ping.
           yield TelemetryController.checkAbortedSessionPing();
 
           // Write the first aborted-session ping as early as possible. Just do that
           // if we are not testing, since calling Telemetry.reset() will make a previous
           // aborted ping a pending ping.
           if (!this._testing) {
--- a/toolkit/components/telemetry/docs/data/crash-ping.rst
+++ b/toolkit/components/telemetry/docs/data/crash-ping.rst
@@ -1,13 +1,24 @@
 
 "crash" ping
 ============
 
-This ping is captured after the main Firefox process crashes, whether or not the crash report is submitted to crash-stats.mozilla.org. It includes non-identifying metadata about the crash.
+This ping is captured after the main Firefox process crashes or after a content
+process crashes, whether or not the crash report is submitted to
+crash-stats.mozilla.org. It includes non-identifying metadata about the crash.
+
+This ping is sent either by the ```CrashManager``` or by the crash reporter
+client. The ```CrashManager``` is responsible for sending crash pings for the
+content process crashes, which are sent right after the crash is detected,
+as well as for main process crashes, which are sent after Firefox restarts
+successfully. The crash reporter client sends crash pings only for main process
+crashes whether or not the user also reports the crash. The crash reporter
+client will not send the crash ping if telemetry has been disabled in Firefox
+though.
 
 The environment block that is sent with this ping varies: if Firefox was running long enough to record the environment block before the crash, then the environment at the time of the crash will be recorded and ``hasCrashEnvironment`` will be true. If Firefox crashed before the environment was recorded, ``hasCrashEnvironment`` will be false and the recorded environment will be the environment at time of submission.
 
 The client ID is submitted with this ping.
 
 Structure:
 
 .. code-block:: js
--- a/toolkit/crashreporter/client/crashreporter.cpp
+++ b/toolkit/crashreporter/client/crashreporter.cpp
@@ -8,19 +8,20 @@
 #ifdef _MSC_VER
 // Disable exception handler warnings.
 # pragma warning( disable : 4530 )
 #endif
 
 #include <fstream>
 #include <sstream>
 #include <memory>
-#include <time.h>
-#include <stdlib.h>
-#include <string.h>
+#include <ctime>
+#include <cstdlib>
+#include <cstring>
+#include <string>
 
 using std::string;
 using std::istream;
 using std::ifstream;
 using std::istringstream;
 using std::ostringstream;
 using std::ostream;
 using std::ofstream;
@@ -191,31 +192,31 @@ static string GetDumpLocalID()
   string::size_type dot = localId.rfind('.');
 
   if (dot == string::npos)
     return "";
 
   return localId.substr(0, dot);
 }
 
-// This appends the StackTraces entry generated by the minidump analyzer to the
-// main crash event so that it can be picked up by Firefox once it restarts
-static void AppendStackTracesToEventFile(const string& aStackTraces)
+// This appends the aKey/aValue entry to the main crash event so that it can
+// be picked up by Firefox once it restarts
+static void AppendToEventFile(const string& aKey, const string& aValue)
 {
   if (gEventsPath.empty()) {
     // If there is no path for finding the crash event, skip this step.
     return;
   }
 
   string localId = GetDumpLocalID();
   string path = gEventsPath + UI_DIR_SEPARATOR + localId;
   ofstream* f = UIOpenWrite(path.c_str(), true);
 
   if (f->is_open()) {
-    *f << "StackTraces=" << aStackTraces;
+    *f << aKey << "=" << aValue << std::endl;
     f->close();
   }
 
   delete f;
 }
 
 static void WriteSubmissionEvent(SubmissionResult result,
                                  const string& remoteId)
@@ -260,18 +261,19 @@ static void OpenLogFile()
 {
   string logPath = gSettingsPath + UI_DIR_SEPARATOR + "submit.log";
   gLogStream.reset(UIOpenWrite(logPath.c_str(), true));
 }
 
 static bool ReadConfig()
 {
   string iniPath;
-  if (!UIGetIniPath(iniPath))
+  if (!UIGetIniPath(iniPath)) {
     return false;
+  }
 
   if (!ReadStringsFromFile(iniPath, gStrings, true))
     return false;
 
   // See if we have a string override file, if so process it
   char* overrideEnv = getenv("MOZ_CRASHREPORTER_STRINGS_OVERRIDE");
   if (overrideEnv && *overrideEnv && UIFileExists(overrideEnv))
     ReadStringsFromFile(overrideEnv, gStrings, true);
@@ -508,31 +510,27 @@ void RewriteStrings(StringTable& queryPa
 
 bool CheckEndOfLifed(string version)
 {
   string reportPath =
     gSettingsPath + UI_DIR_SEPARATOR + "EndOfLife" + version;
   return UIFileExists(reportPath);
 }
 
-#ifndef RELEASE_OR_BETA
-
 static string
-GetMinidumpAnalyzerPath()
+GetProgramPath(const string& exename)
 {
   string path = gArgv[0];
   size_t pos = path.rfind(UI_CRASH_REPORTER_FILENAME BIN_SUFFIX);
   path.erase(pos);
-  path.append(UI_MINIDUMP_ANALYZER_FILENAME BIN_SUFFIX);
+  path.append(exename + BIN_SUFFIX);
 
   return path;
 }
 
-#endif
-
 int main(int argc, char** argv)
 {
   gArgc = argc;
   gArgv = argv;
 
   if (!ReadConfig()) {
     UIError("Couldn't read configuration.");
     return 0;
@@ -547,17 +545,19 @@ int main(int argc, char** argv)
 
   if (gReporterDumpFile.empty()) {
     // no dump file specified, run the default UI
     UIShowDefaultUI();
   } else {
 #ifndef RELEASE_OR_BETA
     // start by running minidump analyzer, this is currently enabled only in
     // nightly and aurora
-    UIRunMinidumpAnalyzer(GetMinidumpAnalyzerPath(), gReporterDumpFile);
+    string empty;
+    UIRunProgram(GetProgramPath(UI_MINIDUMP_ANALYZER_FILENAME),
+                 gReporterDumpFile, empty, /* wait */ true);
 #endif
 
     // go ahead with the crash reporter
     gExtraFile = GetAdditionalFilename(gReporterDumpFile, kExtraDataExtension);
     if (gExtraFile.empty()) {
       UIError(gStrings[ST_ERROR_BADARGUMENTS]);
       return 0;
     }
@@ -635,20 +635,27 @@ int main(int argc, char** argv)
     if (eventsPath && *eventsPath) {
       gEventsPath = eventsPath;
     }
 #endif
     else {
       gEventsPath.clear();
     }
 
+    // Assemble and send the crash ping
+    string pingUuid;
+
+    if (SendCrashPing(queryParameters, pingUuid)) {
+      AppendToEventFile("CrashPingUUID", pingUuid);
+    }
+
     // Update the crash event with stacks if they are present
     auto stackTracesItr = queryParameters.find("StackTraces");
     if (stackTracesItr != queryParameters.end()) {
-      AppendStackTracesToEventFile(stackTracesItr->second);
+      AppendToEventFile(stackTracesItr->first, stackTracesItr->second);
     }
 
     if (!UIFileExists(gReporterDumpFile)) {
       UIError(gStrings[ST_ERROR_DUMPFILEEXISTS]);
       return 0;
     }
 
     string pendingDir = gSettingsPath + UI_DIR_SEPARATOR + "pending";
--- a/toolkit/crashreporter/client/crashreporter.h
+++ b/toolkit/crashreporter/client/crashreporter.h
@@ -34,16 +34,21 @@ std::string WideToUTF8(const std::wstrin
 
 #define UI_SNPRINTF snprintf
 #define UI_DIR_SEPARATOR "/"
 
 #endif
 
 #define UI_CRASH_REPORTER_FILENAME "crashreporter"
 #define UI_MINIDUMP_ANALYZER_FILENAME "minidump-analyzer"
+#ifndef XP_MACOSX
+#define UI_PING_SENDER_FILENAME "pingsender"
+#else
+#define UI_PING_SENDER_FILENAME "../../../pingsender"
+#endif
 
 typedef std::map<std::string, std::string> StringTable;
 
 #define ST_CRASHREPORTERTITLE        "CrashReporterTitle"
 #define ST_CRASHREPORTERVENDORTITLE  "CrashReporterVendorTitle"
 #define ST_CRASHREPORTERERROR        "CrashReporterErrorText"
 #define ST_CRASHREPORTERPRODUCTERROR "CrashReporterProductErrorText2"
 #define ST_CRASHREPORTERHEADER       "CrashReporterSorry"
@@ -74,17 +79,17 @@ typedef std::map<std::string, std::strin
 #define ST_ERROR_DUMPFILEMOVE        "ErrorDumpFileMove"
 #define ST_ERROR_NOPRODUCTNAME       "ErrorNoProductName"
 #define ST_ERROR_NOSERVERURL         "ErrorNoServerURL"
 #define ST_ERROR_NOSETTINGSPATH      "ErrorNoSettingsPath"
 #define ST_ERROR_CREATEDUMPDIR       "ErrorCreateDumpDir"
 #define ST_ERROR_ENDOFLIFE           "ErrorEndOfLife"
 
 //=============================================================================
-// implemented in crashreporter.cpp
+// implemented in crashreporter.cpp and ping.cpp
 //=============================================================================
 
 namespace CrashReporter {
   extern StringTable  gStrings;
   extern std::string  gSettingsPath;
   extern std::string  gEventsPath;
   extern int          gArgc;
   extern char**       gArgv;
@@ -107,16 +112,19 @@ namespace CrashReporter {
   bool WriteStringsToFile(const std::string& path,
                           const std::string& header,
                           StringTable& strings,
                           bool escape);
   void LogMessage(const std::string& message);
   void DeleteDump();
   bool ShouldEnableSending();
 
+  // Telemetry ping
+  bool SendCrashPing(StringTable& strings, std::string& pingUuid);
+
   static const unsigned int kSaveCount = 10;
 }
 
 //=============================================================================
 // implemented in the platform-specific files
 //=============================================================================
 
 bool UIInit();
@@ -143,16 +151,24 @@ bool UIEnsurePathExists(const std::strin
 bool UIFileExists(const std::string& path);
 bool UIMoveFile(const std::string& oldfile, const std::string& newfile);
 bool UIDeleteFile(const std::string& oldfile);
 std::ifstream* UIOpenRead(const std::string& filename);
 std::ofstream* UIOpenWrite(const std::string& filename,
                            bool append=false,
                            bool binary=false);
 void UIPruneSavedDumps(const std::string& directory);
-void UIRunMinidumpAnalyzer(const std::string& exename,
-                           const std::string& filename);
+
+// Run the program specified by exename, passing it the parameter in arg and
+// then sending it the contents of data (if any) via a pipe tied to its stdin.
+// If wait is true, wait for the program to terminate execution before
+// returning. Returns true if the program was launched correctly, false
+// otherwise.
+bool UIRunProgram(const std::string& exename,
+                  const std::string& arg,
+                  const std::string& data,
+                  bool wait = false);
 
 #ifdef _MSC_VER
 # pragma warning( pop )
 #endif
 
 #endif
--- a/toolkit/crashreporter/client/crashreporter_unix_common.cpp
+++ b/toolkit/crashreporter/client/crashreporter_unix_common.cpp
@@ -65,21 +65,72 @@ void UIPruneSavedDumps(const std::string
     // s/.dmp/.extra/
     path.replace(path.size() - 4, 4, ".extra");
     UIDeleteFile(path.c_str());
 
     dumpfiles.pop_back();
   }
 }
 
-void UIRunMinidumpAnalyzer(const string& exename, const string& filename)
+bool UIRunProgram(const std::string& exename, const std::string& arg,
+                  const std::string& data, bool wait)
 {
-  // Run the minidump analyzer and wait for it to finish
+  bool usePipes = !data.empty();
+  int fd[2];
+
+  if (usePipes && (pipe(fd) != 0)) {
+    return false;
+  }
+
   pid_t pid = fork();
 
   if (pid == -1) {
-    return; // Nothing to do upon failure
+    return false;
   } else if (pid == 0) {
-    execl(exename.c_str(), exename.c_str(), filename.c_str(), nullptr);
+    // Child
+    if (usePipes) {
+      if (dup2(fd[0], STDIN_FILENO) == -1) {
+        exit(EXIT_FAILURE);
+      }
+
+      close(fd[0]);
+      close(fd[1]);
+    }
+
+    char* argv[] = {
+      const_cast<char*>(exename.c_str()),
+      const_cast<char*>(arg.c_str()),
+      nullptr
+    };
+
+    // Run the program
+    int rv = execv(exename.c_str(), argv);
+
+    if (rv == -1) {
+      exit(EXIT_FAILURE);
+    }
   } else {
-    waitpid(pid, nullptr, 0);
+    // Parent
+    if (usePipes) {
+      ssize_t rv;
+      size_t offset = 0;
+      size_t size = data.size();
+
+      do {
+        rv = write(fd[1], data.c_str() + offset, size);
+
+        if (rv > 0) {
+          size -= rv;
+          offset += rv;
+        }
+      } while (size && ((rv > 0) || ((rv == -1) && errno == EINTR)));
+
+      close(fd[0]);
+      close(fd[1]);
+    }
+
+    if (wait) {
+      waitpid(pid, nullptr, 0);
+    }
   }
+
+  return true;
 }
--- a/toolkit/crashreporter/client/crashreporter_win.cpp
+++ b/toolkit/crashreporter/client/crashreporter_win.cpp
@@ -1540,29 +1540,86 @@ void UIPruneSavedDumps(const std::string
     // s/.dmp/.extra/
     path.replace(path.size() - 4, 4, L".extra");
     DeleteFile(path.c_str());
 
     dumpfiles.pop_back();
   }
 }
 
-void UIRunMinidumpAnalyzer(const string& exename, const string& filename)
+bool UIRunProgram(const string& exename, const string& arg,
+                  const string& data, bool wait)
 {
-  wstring cmdLine;
+  bool usePipes = !data.empty();
+  HANDLE childStdinPipeRead = nullptr;
+  HANDLE childStdinPipeWrite = nullptr;
+
+  if (usePipes) {
+    // Set up the pipes
+    SECURITY_ATTRIBUTES sa = {};
+    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
+    sa.lpSecurityDescriptor = nullptr;
+    sa.bInheritHandle = true;
 
-  cmdLine += L"\"" + UTF8ToWide(exename) + L"\" ";
-  cmdLine += L"\"" + UTF8ToWide(filename) + L"\" ";
+    // Create a pipe for the child process's stdin and ensure its writable end is
+    // not inherited by the child process.
+    if (!CreatePipe(&childStdinPipeRead, &childStdinPipeWrite, &sa, 0)) {
+      return false;
+    }
+
+    if (!SetHandleInformation(childStdinPipeWrite, HANDLE_FLAG_INHERIT, 0)) {
+      return false;
+    }
+  }
+
+  wstring cmdLine = L"\"" + UTF8ToWide(exename) + L"\" " +
+                    L"\"" + UTF8ToWide(arg) + L"\" ";
 
   STARTUPINFO si = {};
+  si.cb = sizeof(si);
+
+  if (usePipes) {
+    si.dwFlags |= STARTF_USESTDHANDLES;
+    si.hStdInput = childStdinPipeRead;
+    si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+    si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
+  }
+
   PROCESS_INFORMATION pi = {};
 
-  si.cb = sizeof(si);
-  si.dwFlags = STARTF_USESHOWWINDOW;
-  si.wShowWindow = SW_SHOWNORMAL;
+  if (!CreateProcess(/* lpApplicationName */ nullptr,
+                     (LPWSTR)cmdLine.c_str(),
+                     /* lpProcessAttributes */ nullptr,
+                     /* lpThreadAttributes */ nullptr,
+                     usePipes,
+                     NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW,
+                     /* lpEnvironment */ nullptr,
+                     /* lpCurrentDirectory */ nullptr,
+                     &si, &pi)) {
+    return false;
+  }
 
-  if (CreateProcess(nullptr, (LPWSTR)cmdLine.c_str(), nullptr, nullptr, FALSE,
-                    0, nullptr, nullptr, &si, &pi)) {
+  if (usePipes) {
+    size_t offset = 0;
+    size_t size = data.size();
+    while (size > 0) {
+      DWORD written = 0;
+      bool success = WriteFile(childStdinPipeWrite, data.data() + offset, size,
+                               &written, nullptr);
+      if (!success) {
+        break;
+      } else {
+        size -= written;
+        offset += written;
+      }
+    }
+
+    CloseHandle(childStdinPipeWrite);
+  }
+
+  if (wait) {
     WaitForSingleObject(pi.hProcess, INFINITE);
-    CloseHandle(pi.hProcess);
-    CloseHandle(pi.hThread);
   }
+
+  CloseHandle(pi.hProcess);
+  CloseHandle(pi.hThread);
+  return true;
 }
--- a/toolkit/crashreporter/client/moz.build
+++ b/toolkit/crashreporter/client/moz.build
@@ -4,29 +4,39 @@
 # 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/.
 
 if CONFIG['OS_TARGET'] != 'Android':
     Program('crashreporter')
 
     UNIFIED_SOURCES += [
         'crashreporter.cpp',
+        'ping.cpp',
+    ]
+
+    LOCAL_INCLUDES += [
+        '/toolkit/crashreporter/jsoncpp/include',
+    ]
+
+    USE_LIBS += [
+        'jsoncpp',
     ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     UNIFIED_SOURCES += [
         'crashreporter_win.cpp',
     ]
     DEFINES['UNICODE'] = True
     DEFINES['_UNICODE'] = True
     USE_LIBS += [
         'google_breakpad_libxul_s',
     ]
     OS_LIBS += [
         'comctl32',
+        'ole32',
         'shell32',
         'wininet',
         'shlwapi',
     ]
 elif CONFIG['OS_ARCH'] == 'Darwin':
     UNIFIED_SOURCES += [
         'crashreporter_osx.mm',
         'crashreporter_unix_common.cpp',
new file mode 100644
--- /dev/null
+++ b/toolkit/crashreporter/client/ping.cpp
@@ -0,0 +1,323 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "crashreporter.h"
+
+#include <cstring>
+#include <string>
+
+#if defined(XP_LINUX)
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#elif defined(XP_MACOSX)
+#include <CoreFoundation/CoreFoundation.h>
+#elif defined(XP_WIN)
+#include <Objbase.h>
+#endif
+
+#include "json/json.h"
+
+using std::string;
+
+namespace CrashReporter {
+
+struct UUID {
+    uint32_t m0;
+    uint16_t m1;
+    uint16_t m2;
+    uint8_t  m3[8];
+};
+
+// Generates an UUID; the code here is mostly copied from nsUUIDGenerator.cpp
+static string
+GenerateUUID()
+{
+  UUID id = {};
+
+#if defined(XP_WIN) // Windows
+  HRESULT hr = CoCreateGuid((GUID*)&id);
+  if (FAILED(hr)) {
+    return "";
+  }
+#elif defined(XP_MACOSX) // MacOS X
+  CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
+  if (!uuid) {
+    return "";
+  }
+
+  CFUUIDBytes bytes = CFUUIDGetUUIDBytes(uuid);
+  memcpy(&id, &bytes, sizeof(UUID));
+
+  CFRelease(uuid);
+#elif defined(HAVE_ARC4RANDOM_BUF) // Android, BSD, ...
+  arc4random_buf(id, sizeof(UUID));
+#else // Linux
+  int fd = open("/dev/urandom", O_RDONLY);
+
+  if (fd == -1) {
+    return "";
+  }
+
+  if (read(fd, &id, sizeof(UUID)) != sizeof(UUID)) {
+    close(fd);
+    return "";
+  }
+
+  close(fd);
+#endif
+
+  /* Put in the version */
+  id.m2 &= 0x0fff;
+  id.m2 |= 0x4000;
+
+  /* Put in the variant */
+  id.m3[0] &= 0x3f;
+  id.m3[0] |= 0x80;
+
+  const char* kUUIDFormatString =
+    "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x";
+  const size_t kUUIDFormatStringLength = 36;
+  char str[kUUIDFormatStringLength + 1] = { '\0' };
+
+  int num = snprintf(str, kUUIDFormatStringLength + 1, kUUIDFormatString,
+                     id.m0, id.m1, id.m2, id.m3[0], id.m3[1], id.m3[2],
+                     id.m3[3], id.m3[4], id.m3[5], id.m3[6], id.m3[7]);
+
+  if (num != kUUIDFormatStringLength) {
+    return "";
+  }
+
+  return str;
+}
+
+const char kISO8601Date[] = "%F";
+const char kISO8601FullDate[] = "%FT%T.000Z";
+
+// Return the current date as a string in the specified format, the following
+// constants are provided:
+// - kISO8601Date, the ISO 8601 date format, YYYY-MM-DD
+// - kISO8601FullDate, the ISO 8601 full date format, YYYY-MM-DDTHH:MM:SS.000Z
+static string
+CurrentDate(string format)
+{
+  time_t now;
+  time(&now);
+  char buf[64]; // This should be plenty
+  strftime(buf, sizeof buf, format.c_str(), gmtime(&now));
+  return buf;
+}
+
+const char kTelemetryClientId[]  = "TelemetryClientId";
+const char kTelemetryUrl[]       = "TelemetryServerURL";
+const char kTelemetrySessionId[] = "TelemetrySessionId";
+const int  kTelemetryVersion     = 4;
+
+// Create the payload.metadata node of the crash ping using fields extracted
+// from the .extra file
+static Json::Value
+CreateMetadataNode(StringTable& strings)
+{
+  // The following list should be kept in sync with the one in CrashManager.jsm
+  const char *entries[] = {
+    "AvailablePageFile",
+    "AvailablePhysicalMemory",
+    "AvailableVirtualMemory",
+    "BlockedDllList",
+    "BlocklistInitFailed",
+    "BuildID",
+    "ContainsMemoryReport",
+    "CrashTime",
+    "EventLoopNestingLevel",
+    "IsGarbageCollecting",
+    "MozCrashReason",
+    "OOMAllocationSize",
+    "ProductID",
+    "ProductName",
+    "ReleaseChannel",
+    "SecondsSinceLastCrash",
+    "StartupCrash",
+    "SystemMemoryUsePercentage",
+    "TelemetrySessionId",
+    "TextureUsage",
+    "TotalPageFile",
+    "TotalPhysicalMemory",
+    "TotalVirtualMemory",
+    "UptimeTS",
+    "User32BeforeBlocklist",
+    "Version",
+  };
+
+  Json::Value node;
+
+  for (auto entry : entries) {
+    if ((strings.find(entry) != strings.end()) && !strings[entry].empty()) {
+      node[entry] = strings[entry];
+    }
+  }
+
+  return node;
+}
+
+// Create the payload node of the crash ping
+static Json::Value
+CreatePayloadNode(StringTable& strings, const string& aSessionId)
+{
+  Json::Value payload;
+
+  payload["sessionId"] = aSessionId;
+  payload["version"] = 1;
+  payload["crashDate"] = CurrentDate(kISO8601Date);
+  payload["hasCrashEnvironment"] = true;
+  payload["crashId"] = GetDumpLocalID();
+  payload["processType"] = "main"; // This is always a main crash
+
+  // Parse the stack traces
+  Json::Value stackTracesValue;
+  Json::Reader reader;
+
+  if (reader.parse(strings["StackTraces"], stackTracesValue,
+                   /* collectComments */ false)) {
+    payload["stackTraces"] = stackTracesValue;
+  }
+
+  // Assemble the payload metadata
+  payload["metadata"] = CreateMetadataNode(strings);
+
+  return payload;
+}
+
+// Create the application node of the crash ping
+static Json::Value
+CreateApplicationNode(const string& aVendor, const string& aName,
+                      const string& aVersion, const string& aChannel,
+                      const string& aBuildId, const string& aArchitecture,
+                      const string& aXpcomAbi)
+{
+  Json::Value application;
+
+  application["vendor"] = aVendor;
+  application["name"] = aName;
+  application["buildId"] = aBuildId;
+  application["displayVersion"] = aVersion;
+  application["platformVersion"] = aVersion;
+  application["version"] = aVersion;
+  application["channel"] = aChannel;
+  if (!aArchitecture.empty()) {
+    application["architecture"] = aArchitecture;
+  }
+  if (!aXpcomAbi.empty()) {
+    application["xpcomAbi"] = aXpcomAbi;
+  }
+
+  return application;
+}
+
+// Create the root node of the crash ping
+static Json::Value
+CreateRootNode(StringTable& strings, const string& aUuid,
+               const string& aClientId, const string& aSessionId,
+               const string& aName, const string& aVersion,
+               const string& aChannel, const string& aBuildId)
+{
+  Json::Value root;
+  root["type"] = "crash"; // This is a crash ping
+  root["id"] = aUuid;
+  root["version"] = kTelemetryVersion;
+  root["creationDate"] = CurrentDate(kISO8601FullDate);
+  root["clientId"] = aClientId;
+
+  // Parse the telemetry environment
+  Json::Value environment;
+  Json::Reader reader;
+  string architecture;
+  string xpcomAbi;
+
+  if (reader.parse(strings["TelemetryEnvironment"], environment,
+                   /* collectComments */ false)) {
+    if (environment.isMember("build") && environment["build"].isObject()) {
+      Json::Value build = environment["build"];
+      if (build.isMember("architecture") && build["architecture"].isString()) {
+        architecture = build["architecture"].asString();
+      }
+      if (build.isMember("xpcomAbi") && build["xpcomAbi"].isString()) {
+        xpcomAbi = build["xpcomAbi"].asString();
+      }
+    }
+
+    root["environment"] = environment;
+  }
+
+  root["payload"] = CreatePayloadNode(strings, aSessionId);
+  root["application"] = CreateApplicationNode(strings["Vendor"], aName,
+                                              aVersion, aChannel, aBuildId,
+                                              architecture, xpcomAbi);
+
+  return root;
+}
+
+// Generates the URL used to submit the crash ping, see TelemetrySend.jsm
+string
+GenerateSubmissionUrl(const string& aUrl, const string& aId,
+                      const string& aName, const string& aVersion,
+                      const string& aChannel, const string& aBuildId)
+{
+  return aUrl + "/submit/telemetry/" + aId + "/crash/" + aName + "/" +
+         aVersion + "/" + aChannel + "/" + aBuildId +
+         "?v=" + std::to_string(kTelemetryVersion);
+}
+
+// Assembles the crash ping using the strings extracted from the .extra file
+// and sends it using the crash sender. All the telemetry specific data but the
+// environment will be stripped from the annotations so that it won't be sent
+// together with the crash report.
+//
+// Note that the crash ping sender is invoked in a fire-and-forget way so this
+// won't block waiting for the ping to be delivered.
+//
+// Returns true if the ping was assembled and handed over to the pingsender
+// correctly, false otherwise and populates the aUUID field with the ping UUID.
+bool
+SendCrashPing(StringTable& strings, string& pingUuid)
+{
+  string clientId    = strings[kTelemetryClientId];
+  string serverUrl   = strings[kTelemetryUrl];
+  string sessionId   = strings[kTelemetrySessionId];
+
+  // Remove the telemetry-related data from the crash annotations
+  strings.erase(kTelemetryClientId);
+  strings.erase(kTelemetryUrl);
+  strings.erase(kTelemetrySessionId);
+
+  string buildId     = strings["BuildID"];
+  string channel     = strings["ReleaseChannel"];
+  string name        = strings["ProductName"];
+  string version     = strings["Version"];
+  string uuid        = GenerateUUID();
+  string url         = GenerateSubmissionUrl(serverUrl, uuid, name, version,
+                                             channel, buildId);
+
+  if (serverUrl.empty() || uuid.empty()) {
+    return false;
+  }
+
+  Json::Value root = CreateRootNode(strings, uuid, clientId, sessionId,
+                                    name, version, channel, buildId);
+
+  // Write out the result
+  Json::FastWriter writer;
+  string ping = writer.write(root);
+
+  // Hand over the ping to the sender
+  if (UIRunProgram(GetProgramPath(UI_PING_SENDER_FILENAME), url, ping)) {
+    pingUuid = uuid;
+    return true;
+  } else {
+    return false;
+  }
+}
+
+} // namespace crashreporter
+
--- a/toolkit/crashreporter/docs/index.rst
+++ b/toolkit/crashreporter/docs/index.rst
@@ -33,16 +33,22 @@ Crash Reporter Client
    Mozilla (or the configured server).
 
 Minidump Analyzer
    The minidump analyzer is a standalone executable that is launched by the
    crash reporter client or by the browser itself to extract stack traces from
    the dump files generated during a crash. It appends the stack traces to the
    .extra file associated with the crash dump.
 
+Ping Sender
+   The ping sender is a standalone executable that is launched by the crash
+   reporter client to deliver a crash ping to our telemetry servers. The ping
+   sender is used to speed up delivery of the crash ping which would otherwise
+   have to wait for Firefox to be restarted in order to be sent.
+
 How Main-Process Crash Handling Works
 =====================================
 
 The crash handler is hooked up very early in the Gecko process lifetime.
 It all starts in ``XREMain::XRE_mainInit()`` from ``nsAppRunner.cpp``.
 Assuming crash reporting is enabled, this startup function registers an
 exception handler for the process and tells the crash reporter subsystem
 about basic metadata such as the application name and version.
@@ -85,17 +91,23 @@ the time of the crash, memory statistics
 If the *crash reporter client* is enabled, ``MinidumpCallback()`` invokes
 it. It simply tries to create a new *crash reporter client* process (e.g.
 *crashreporter.exe*) with the path to the written minidump file as an
 argument.
 
 The *crash reporter client* performs a number of roles. There's a lot going
 on, so you may want to look at ``main()`` in ``crashreporter.cpp``. First,
 stack traces are extracted from the dump via the *minidump analyzer* tool.
-The resulting traces are appended to the .extra file of the crash. Then, the
+The resulting traces are appended to the .extra file of the crash. Once this
+is done a crash ping is assembled holding the same information as the one
+generated by the ```CrashManager``` and it's sent to the telemetry servers via
+the *ping sender* program. The UUID of the ping is then stored in the extra
+file; the ```CrashManager``` will later pick it up and generate a new ping
+with the same UUID so that the telemetry server can deduplicate both pings.
+Then, the
 *crash reporter client* verifies that the dump data is sane. If it isn't
 (e.g. required metadata is missing), the dump data is ignored. If dump data
 looks sane, the dump data
 is moved into the *pending* directory for the configured data directory
 (defined via the ``MOZ_CRASHREPORTER_DATA_DIRECTORY`` environment variable
 or from the UI). Once this is done, the main crash reporter UI is displayed
 via ``UIShowCrashUI()``. The crash reporter UI is platform specific: there
 are separate versions for Windows, OS X, and various \*NIX presentation
--- a/toolkit/crashreporter/nsExceptionHandler.cpp
+++ b/toolkit/crashreporter/nsExceptionHandler.cpp
@@ -1092,33 +1092,36 @@ bool MinidumpCallback(
 #endif
 
       eventFile.Open(crashEventPath);
       WriteLiteral(eventFile, kCrashMainID);
       WriteString(eventFile, crashTimeString);
       WriteLiteral(eventFile, "\n");
       WriteString(eventFile, id_ascii);
       WriteLiteral(eventFile, "\n");
-      if (currentSessionId) {
-        WriteAnnotation(eventFile, "TelemetrySessionId", currentSessionId);
-      }
       if (crashEventAPIData) {
         eventFile.WriteBuffer(crashEventAPIData->get(), crashEventAPIData->Length());
       }
     }
 
     if (!crashReporterAPIData->IsEmpty()) {
       // write out API data
 #ifdef XP_LINUX
       OpenAPIData(apiData, descriptor.path());
 #else
       OpenAPIData(apiData, dump_path, minidump_id);
 #endif
       apiData.WriteBuffer(crashReporterAPIData->get(), crashReporterAPIData->Length());
     }
+
+    if (currentSessionId) {
+      WriteAnnotation(apiData, "TelemetrySessionId", crashTimeString);
+      WriteAnnotation(eventFile, "TelemetrySessionId", currentSessionId);
+    }
+
     WriteAnnotation(apiData, "CrashTime", crashTimeString);
     WriteAnnotation(eventFile, "CrashTime", crashTimeString);
 
     WriteAnnotation(apiData, "UptimeTS", uptimeTSString);
     WriteAnnotation(eventFile, "UptimeTS", uptimeTSString);
 
     if (timeSinceLastCrash != 0) {
       WriteAnnotation(apiData, "SecondsSinceLastCrash",
--- a/toolkit/crashreporter/test/unit/test_crashreporter_crash.js
+++ b/toolkit/crashreporter/test/unit/test_crashreporter_crash.js
@@ -40,11 +40,35 @@ function run_test() {
              let scope = {};
              Components.utils.import("resource://gre/modules/TelemetryController.jsm", scope);
              scope.TelemetryController.testSetup();
            },
            function(mdump, extra) {
              do_check_eq(extra.TestKey, "TestValue");
              do_check_eq(extra["\u2665"], "\u{1F4A9}");
              do_check_eq(extra.Notes, "JunkMoreJunk");
-             do_check_true(!("TelemetrySessionId" in extra));
+             do_check_true("TelemetrySessionId" in extra);
+             Assert.ok(
+              "TelemetryServerURL" in extra,
+              "The TelemetryServerURL field is omitted when telemetry is off"
+             );
            });
+
+  do_crash(function() {
+    // Enable telemetry
+    let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+                          .getService(Components.interfaces.nsIPrefBranch);
+
+    // Turn on telemetry and specify a telemetry server
+    prefs.setCharPref("toolkit.telemetry.server", "http://a.telemetry.server");
+    prefs.setBoolPref("toolkit.telemetry.enabled", true);
+
+    // TelemetrySession setup will trigger the session annotation
+    let scope = {};
+    Components.utils.import("resource://gre/modules/TelemetryController.jsm", scope);
+    scope.TelemetryController.testSetup();
+  }, function(mdump, extra) {
+    Assert.ok("TelemetryServerURL" in extra,
+              "The TelemetryServerURL field is present in the extra file");
+    Assert.equal(extra.TelemetryServerURL, "http://a.telemetry.server",
+                  "The TelemetryServerURL field is properly set");
+  });
 }