Bug 1310703 - Add support for the pingsender to the Telemetry code; r?Dexter
MozReview-Commit-ID: 6XcljmJCbFk
--- 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;
@@ -2645,16 +2654,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)