Bug 1310703 - Add support for the pingsender to the Telemetry code; r?Dexter draft
authorGabriele Svelto <gsvelto@mozilla.com>
Tue, 17 Jan 2017 14:53:00 +0100
changeset 479393 36ddc951b9dd21ef76670bee32bdbe2deeafb8c2
parent 479392 b05a5cd14c4d52d36937189d159571f8487dc0de
child 544670 28d5f1b6952437822484025117e942c2fabca849
push id44239
push userbmo:gsvelto@mozilla.com
push dateMon, 06 Feb 2017 15:52:30 +0000
reviewersDexter
bugs1310703
milestone54.0a1
Bug 1310703 - Add support for the pingsender to the Telemetry code; r?Dexter MozReview-Commit-ID: 6XcljmJCbFk
toolkit/components/telemetry/Telemetry.cpp
toolkit/components/telemetry/moz.build
toolkit/components/telemetry/nsITelemetry.idl
toolkit/components/telemetry/tests/unit/head.js
toolkit/components/telemetry/tests/unit/test_PingSender.js
toolkit/components/telemetry/tests/unit/test_TelemetryController.js
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
toolkit/components/telemetry/tests/unit/xpcshell.ini
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -4,23 +4,25 @@
  * 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 <algorithm>
 
 #include <fstream>
 
 #include <prio.h>
+#include <prproces.h>
 
 #include "mozilla/dom/ToJSValue.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Likely.h"
 #include "mozilla/MathAlgorithms.h"
+#include "mozilla/Scoped.h"
 #include "mozilla/Unused.h"
 
 #include "base/pickle.h"
 #include "nsIComponentManager.h"
 #include "nsIServiceManager.h"
 #include "nsThreadManager.h"
 #include "nsCOMArray.h"
 #include "nsCOMPtr.h"
@@ -77,16 +79,23 @@
 
 #if defined(MOZ_GECKO_PROFILER)
 #include "shared-libraries.h"
 #define ENABLE_STACK_CAPTURE
 #include "mozilla/StackWalk.h"
 #include "nsPrintfCString.h"
 #endif // MOZ_GECKO_PROFILER
 
+namespace mozilla {
+  // Scoped auto-close for PRFileDesc file descriptors
+  MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc,
+                                            PRFileDesc,
+                                            PR_Close);
+}
+
 namespace {
 
 using namespace mozilla;
 using namespace mozilla::HangMonitor;
 using Telemetry::Common::AutoHashtable;
 
 // The maximum number of chrome hangs stacks that we're keeping.
 const size_t kMaxChromeStacksKept = 50;
@@ -2644,16 +2653,104 @@ TelemetryImpl::SetEventRecordingEnabled(
 
 NS_IMETHODIMP
 TelemetryImpl::FlushBatchedChildTelemetry()
 {
   TelemetryIPCAccumulator::IPCTimerFired(nullptr, nullptr);
   return NS_OK;
 }
 
+#ifndef MOZ_WIDGET_ANDROID
+
+static nsresult
+LocatePingSender(nsAString& aPath)
+{
+  nsCOMPtr<nsIFile> xreAppDistDir;
+  nsresult rv = NS_GetSpecialDirectory(XRE_APP_DISTRIBUTION_DIR,
+                                       getter_AddRefs(xreAppDistDir));
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  xreAppDistDir->AppendNative(NS_LITERAL_CSTRING("pingsender" BIN_SUFFIX));
+  xreAppDistDir->GetPath(aPath);
+  return NS_OK;
+}
+
+#endif // MOZ_WIDGET_ANDROID
+
+NS_IMETHODIMP
+TelemetryImpl::RunPingSender(const nsACString& aUrl, const nsACString& aPing)
+{
+#ifdef MOZ_WIDGET_ANDROID
+  Unused << aUrl;
+  Unused << aPing;
+
+  return NS_ERROR_NOT_IMPLEMENTED;
+#else // Windows, Mac, Linux, etc...
+  // Obtain the path of the pingsender executable
+  nsAutoString path;
+  nsresult rv = LocatePingSender(path);
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Create a pipe to send the ping contents to the ping sender
+  ScopedPRFileDesc pipeRead;
+  ScopedPRFileDesc pipeWrite;
+
+  if (PR_CreatePipe(&pipeRead.rwget(), &pipeWrite.rwget()) != PR_SUCCESS) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if ((PR_SetFDInheritable(pipeRead, PR_TRUE) != PR_SUCCESS) ||
+      (PR_SetFDInheritable(pipeWrite, PR_FALSE) != PR_SUCCESS)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  PRProcessAttr* attr = PR_NewProcessAttr();
+  if (!attr) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Connect the pingsender standard input to the pipe and launch it
+  PR_ProcessAttrSetStdioRedirect(attr, PR_StandardInput, pipeRead);
+
+  UniquePtr<char[]> arg0(ToNewCString(path));
+  UniquePtr<char[]> arg1(ToNewCString(aUrl));
+
+  char* args[] = {
+    arg0.get(),
+    arg1.get(),
+    nullptr,
+  };
+  Unused << NS_WARN_IF(PR_CreateProcessDetached(args[0], args, nullptr, attr));
+  PR_DestroyProcessAttr(attr);
+
+  // Send the ping contents to the ping sender
+  size_t length = aPing.Length();
+  const char* s = aPing.BeginReading();
+
+  while (length > 0) {
+    int result = PR_Write(pipeWrite, s, length);
+
+    if (result <= 0) {
+      return NS_ERROR_FAILURE;
+    }
+
+    s += result;
+    length -= result;
+  }
+
+  return NS_OK;
+#endif
+}
+
 size_t
 TelemetryImpl::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
 {
   size_t n = aMallocSizeOf(this);
 
   // Ignore the hashtables in mAddonMap; they are not significant.
   n += TelemetryHistogram::GetMapShallowSizesOfExcludingThis(aMallocSizeOf);
   n += TelemetryScalar::GetMapShallowSizesOfExcludingThis(aMallocSizeOf);
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -10,16 +10,17 @@ include('/ipc/chromium/chromium-config.m
 
 FINAL_LIBRARY = 'xul'
 
 DIRS = [
     'pingsender',
 ]
 
 DEFINES['MOZ_APP_VERSION'] = '"%s"' % CONFIG['MOZ_APP_VERSION']
+DEFINES['BIN_SUFFIX'] = '"%s"' % CONFIG['BIN_SUFFIX']
 
 LOCAL_INCLUDES += [
     '/xpcom/build',
     '/xpcom/threads',
 ]
 
 SPHINX_TREES['telemetry'] = 'docs'
 
--- a/toolkit/components/telemetry/nsITelemetry.idl
+++ b/toolkit/components/telemetry/nsITelemetry.idl
@@ -512,9 +512,23 @@ interface nsITelemetry : nsISupports
    */
   [implicit_jscontext, optional_argc]
   jsval snapshotBuiltinEvents(in uint32_t aDataset, [optional] in boolean aClear);
 
   /**
    * Resets all the stored events. This is intended to be only used in tests.
    */
   void clearEvents();
+
+  /**
+   * Send a ping using the ping sender.
+   * This method will not wait for the ping to be sent, instead it will return
+   * as soon as the contents of the ping have been handed over to the ping
+   * sender.
+   *
+   * @param aUrl The telemetry server URL
+   * @param aPing A string holding the ping contents
+   *
+   * @throws NS_ERROR_FAILURE if we couldn't run the pingsender or if we
+   *         couldn't hand it the ping data
+   */
+  void runPingSender(in ACString aUrl, in ACString aPing);
 };
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -117,17 +117,18 @@ const PingServer = {
  * @param {Object} request The data representing an HTTP request (nsIHttpRequest).
  * @return {Object} The decoded ping payload.
  */
 function decodeRequestPayload(request) {
   let s = request.bodyInputStream;
   let payload = null;
   let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON)
 
-  if (request.getHeader("content-encoding") == "gzip") {
+  if (request.hasHeader("content-encoding") &&
+      request.getHeader("content-encoding") == "gzip") {
     let observer = {
       buffer: "",
       onStreamComplete(loader, context, status, length, result) {
         this.buffer = String.fromCharCode.apply(this, result);
       }
     };
 
     let scs = Cc["@mozilla.org/streamConverters;1"]
@@ -295,24 +296,16 @@ function setEmptyPrefWatchlist() {
   let TelemetryEnvironment =
     Cu.import("resource://gre/modules/TelemetryEnvironment.jsm").TelemetryEnvironment;
   return TelemetryEnvironment.onInitialized().then(() => {
     TelemetryEnvironment.testWatchPreferences(new Map());
 
   });
 }
 
-// Generate a UUID, used for the ping ID
-function generateUUID() {
-  let str = Cc["@mozilla.org/uuid-generator;1"]
-              .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
-  // strip {}
-  return str.substring(1, str.length - 1);
-}
-
 if (runningInParent) {
   // Set logging preferences for all the tests.
   Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
   // Telemetry archiving should be on.
   Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
   // Telemetry xpcshell tests cannot show the infobar.
   Services.prefs.setBoolPref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
   // FHR uploads should be enabled.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_PingSender.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests submitting a ping using the stand-alone pingsender program.
+
+"use strict";
+
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/osfile.jsm", this);
+
+add_task(function* test_pingSender() {
+  // Make sure the code can find the pingsender executable
+  const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
+  let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+  dir.initWithPath(OS.Constants.Path.libDir);
+
+  Services.dirsvc.registerProvider({
+    getFile(aProp, aPersistent) {
+      aPersistent.value = true;
+      if (aProp == XRE_APP_DISTRIBUTION_DIR) {
+        return dir.clone();
+      }
+      return null;
+    }
+  });
+
+  PingServer.start();
+
+  const url = "http://localhost:" + PingServer.port + "/submit/telemetry/";
+  const data = {
+    type: "test-pingsender-type",
+    id: TelemetryUtils.generateUUID(),
+    creationDate: (new Date(1485810000)).toISOString(),
+    version: 4,
+    payload: {
+      dummy: "stuff"
+    }
+  };
+
+  Telemetry.runPingSender(url, JSON.stringify(data));
+
+  let ping = yield PingServer.promiseNextPing();
+
+  Assert.equal(ping.id, data.id, "Should have received the correct ping id.");
+  Assert.equal(ping.type, data.type,
+               "Should have received the correct ping type.");
+  Assert.deepEqual(ping.payload, data.payload,
+                   "Should have received the correct payload.");
+});
+
+add_task(function* cleanup() {
+  yield PingServer.stop();
+});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -10,16 +10,17 @@
 
 Cu.import("resource://gre/modules/ClientID.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
 Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
 
 const PING_FORMAT_VERSION = 4;
 const DELETION_PING_TYPE = "deletion";
 const TEST_PING_TYPE = "test-ping-type";
 
@@ -284,17 +285,17 @@ add_task(function* test_pingHasEnvironme
   // Test a field in the environment build section.
   Assert.equal(ping.application.buildId, ping.environment.build.buildId);
   // Test that we have the correct clientId.
   Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");
 });
 
 add_task(function* test_pingIdCanBeOverridden() {
   // Send a ping with an overridden id
-  const myPingId = generateUUID();
+  const myPingId = TelemetryUtils.generateUUID();
   yield sendPing(/* aSendClientId */ false,
                  /* aSendEnvironment */ false,
                  myPingId);
   let ping = yield PingServer.promiseNextPing();
   checkPingFormat(ping, TEST_PING_TYPE, false, false);
 
   Assert.equal(ping.id, myPingId, "The ping id must be the one we provided.");
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -13,16 +13,17 @@ Cu.import("resource://gre/modules/Client
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
 Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
 Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/osfile.jsm", this);
 
 const PING_FORMAT_VERSION = 4;
 const PING_TYPE_MAIN = "main";
 const PING_TYPE_SAVED_SESSION = "saved-session";
@@ -595,17 +596,17 @@ add_task(function* test_simplePing() {
   Assert.equal(payload.info.subsessionId, expectedSubsessionUUID);
   let sessionStartDate = new Date(payload.info.sessionStartDate);
   Assert.equal(sessionStartDate.toISOString(), expectedDate.toISOString());
   let subsessionStartDate = new Date(payload.info.subsessionStartDate);
   Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
   Assert.equal(payload.info.subsessionLength, SESSION_DURATION_IN_MINUTES * 60);
 
   // Restore the UUID generator so we don't mess with other tests.
-  fakeGenerateUUID(generateUUID, generateUUID);
+  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
 });
 
 // Saves the current session histograms, reloads them, performs a ping
 // and checks that the dummy http server received both the previously
 // saved ping and the new one.
 add_task(function* test_saveLoadPing() {
   // Let's start out with a defined state.
   yield TelemetryStorage.testClearPendingPings();
@@ -1348,17 +1349,17 @@ add_task(function* test_savedSessionData
   yield changePromise;
   TelemetryEnvironment.unregisterChangeListener("test_fake_change");
 
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
   yield TelemetryController.testShutdown();
 
   // Restore the UUID generator so we don't mess with other tests.
-  fakeGenerateUUID(generateUUID, generateUUID);
+  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
   Assert.equal(data.sessionId, expectedSessionUUID);
   Assert.equal(data.subsessionId, expectedSubsessionUUID);
 });
 
@@ -1386,17 +1387,17 @@ add_task(function* test_sessionData_Shor
   TelemetryController.testReset();
   yield TelemetryController.testShutdown();
 
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
 
   // Restore the UUID generation functions.
-  fakeGenerateUUID(generateUUID, generateUUID);
+  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
 
   // Start TelemetryController so that it loads the session data file. We expect the profile
   // subsession counter to be incremented by 1 again.
   yield TelemetryController.testReset();
 
   // We expect 2 profile subsession counter updates.
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, 2);
@@ -1449,17 +1450,17 @@ add_task(function* test_invalidSessionDa
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
 
   yield TelemetryController.testShutdown();
 
   // Restore the UUID generator so we don't mess with other tests.
-  fakeGenerateUUID(generateUUID, generateUUID);
+  fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
   Assert.equal(data.sessionId, expectedSessionUUID);
   Assert.equal(data.subsessionId, expectedSubsessionUUID);
 });
 
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -58,8 +58,10 @@ tags = addons
 skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
 [test_TelemetryReportingPolicy.js]
 tags = addons
 [test_TelemetryScalars.js]
 [test_TelemetryTimestamps.js]
 skip-if = toolkit == 'android'
 [test_TelemetryCaptureStack.js]
 [test_TelemetryEvents.js]
+[test_PingSender.js]
+skip-if = (os == "android") || (os == "linux" && bits == 32)