Bug 1307568 - add a diagnostic system add-on to measure uptake of various updates r?kmag
MozReview-Commit-ID: 71uSNlGcfQt
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/bootstrap.js
@@ -0,0 +1,277 @@
+/* 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/CertUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/Log.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
+ "resource://gre/modules/TelemetryLog.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+const expectedUpdateURL = "https://aus5.mozilla.org/update/3/SystemAddons/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml";
+const diagnosticsFile = "diagnostics-data.xpi";
+const diagnosticsPref = "extensions.diagnostics.v2.hasRun";
+const diagnosticsUrlPref = "extensions.systemAddon.diagnostics.url";
+const defaultDiagnosticsUrl = "https://ftp.mozilla.org/pub/system-addons/diagnostics/";
+const systemAddonUrlPref = "extensions.systemAddon.update.url";
+const diagnosticsChecksum = "a3fd8615a1b8dd8437ac1c0ef68ed4cf8a3795dfa53df02a6c309b1df92be9d3522a4d2e588d86289ed05c3103750e053cf09fd1c5c27cb6dcb9d2e59445afac";
+const hashFunction = "sha512";
+const TIMEOUT_DELAY_MS = 20000;
+
+function startup() {
+ let hasRun = Preferences.get(diagnosticsPref, false);
+ if (!hasRun) {
+ testSystemAddons();
+ }
+}
+function shutdown() {}
+function install() {}
+function uninstall() {
+ Preferences.reset(diagnosticsPref);
+}
+
+/**
+ * Convert a string containing binary values to hex.
+ * NOTE - originally from ProductAddonChecker
+ */
+function binaryToHex(input) {
+ let result = "";
+ for (let i = 0; i < input.length; ++i) {
+ let hex = input.charCodeAt(i).toString(16);
+ if (hex.length == 1) {
+ hex = "0" + hex;
+ }
+ result += hex;
+ }
+ return result;
+}
+
+async function testSystemAddons() {
+ // 1: check that update URL is set to expected value.
+ const updateUrl = Preferences.get(systemAddonUrlPref);
+ if (updateUrl === expectedUpdateURL) {
+ TelemetryLog.log("DIAGNOSTICS_SUCCESS_EXPECTED_UPDATE_URL");
+ } else {
+ TelemetryLog.log("DIAGNOSTICS_ERROR_UNEXPECTED_UPDATE_URL", [updateUrl]);
+ }
+
+ // 2: try to connect to AUS.
+ const url = UpdateUtils.formatUpdateURL(updateUrl);
+ // try with default settings.
+ let serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"]
+ .createInstance();
+ await downloadXML(url).then(
+ xml => TelemetryLog.log("DIAGNOSTICS_SUCCESS_AUS_BUILT_IN_CERT",
+ [serializer.serializeToString(xml)]),
+ async function(err) {
+ TelemetryLog.log("DIAGNOSTICS_ERROR_AUS_BUILT_IN_CERT",
+ [JSON.stringify(err, ["message", "arguments", "type", "name"])]);
+
+ // try allowing non-built-in certs.
+ await downloadXML(url, true).then(
+ xml => TelemetryLog.log("DIAGNOSTICS_SUCCESS_AUS_NON_BUILT_IN_CERT",
+ [serializer.serializeToString(xml)]),
+ err => TelemetryLog.log("DIAGNOSTICS_ERROR_AUS_NON_BUILT_IN_CERT",
+ [JSON.stringify(err, ["message", "arguments", "type", "name"])])
+ );
+ }
+ );
+
+ // 3: check if system add-on update directories are present.
+ // if system addon update dir exists, ensure it is r/w.
+ const updatesDir = await FileUtils.getDir("ProfD", ["features"], false);
+ if (updatesDir.exists()) {
+ if (updatesDir.isReadable() && updatesDir.isWritable()) {
+ TelemetryLog.log("DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_UPDATES_DIR_RW")
+ /* TODO if system addon updates are present, ensure they are r/w.
+ if (true) {
+ TelemetryLog.log("DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_UPDATES_RW")
+ } else {
+ TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_UPDATES_NOT_RW")
+ }
+ */
+ } else {
+ TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_UPDATES_DIR_NOT_RW", updatesDir.permissions);
+ }
+ } else {
+ TelemetryLog.log("DIAGNOSTICS_WARN_SYSTEM_ADDON_UPDATES_DIR_DOES_NOT_EXIST");
+ }
+
+ // 4: connect to artifact server, download and check sha/size of deliverable
+ let diagnosticsUrl = Preferences.get(diagnosticsUrlPref, defaultDiagnosticsUrl);
+ diagnosticsUrl += diagnosticsFile;
+
+ // NOTE - based on computeHash in ProductAddonChecker.jsm
+ await downloadFile(diagnosticsUrl).then(
+ async function(path) {
+ const HASH_CHUNK_SIZE = 8192;
+
+ let file = await OS.File.open(path, { existing: true, read: true });
+ try {
+ TelemetryLog.log("DIAGNOSTICS_SUCCESS_DOWNLOADED_SYSTEM_ADDON");
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.initWithString(hashFunction);
+ let bytes;
+ do {
+ bytes = await file.read(HASH_CHUNK_SIZE);
+ await hasher.update(bytes, bytes.length);
+ } while (bytes.length == HASH_CHUNK_SIZE);
+ let result = binaryToHex(hasher.finish(false));
+ if (result) {
+ TelemetryLog.log("DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_CHECKSUM_MATCHES");
+ } else {
+ TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_BAD_CHECKSUM");
+ }
+ } catch(e) {
+ TelemetryLog.log("DIAGNOSTICS_ERROR_SYSTEM_ADDON_COMPUTING_CHECKSUM_FAILED");
+ } finally {
+ await file.close();
+ }
+ await file.remove(false);
+ },
+ err => TelemetryLog.log("DIAGNOSTICS_ERROR_COULD_NOT_DOWNLOAD_SYSTEM_ADDON",
+ [JSON.stringify(err, ["message", "arguments", "type", "name"])])
+ );
+
+ Preferences.set(diagnosticsPref, true);
+}
+
+/**
+ * Downloads file from a URL using XHR.
+ * NOTE - based on ProductAddonChecker.jsm
+ *
+ * @param url
+ * The url to download from.
+ * @return a promise that resolves to the path of a temporary file or rejects
+ * with a JS exception in case of error.
+ */
+function downloadFile(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsISupports);
+ xhr.onload = function(response) {
+ //console.info("downloadXHR File download. status=" + xhr.status);
+ if (xhr.status != 200 && xhr.status != 206) {
+ reject(Components.Exception("File download failed", xhr.status));
+ return;
+ }
+ Task.spawn(function* () {
+ let f = yield OS.File.openUnique(OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon"));
+ let path = f.path;
+ //console.info(`Downloaded file will be saved to ${path}`);
+ yield f.file.close();
+ yield OS.File.writeAtomic(path, new Uint8Array(xhr.response));
+ return path;
+ }).then(resolve, reject);
+ };
+
+ let fail = (event) => {
+ let request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading via XHR, status: " + status + ", reason: " + event.type;
+ //console.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+ xhr.addEventListener("error", fail);
+ xhr.addEventListener("abort", fail);
+
+ xhr.responseType = "arraybuffer";
+ xhr.open("GET", url);
+ xhr.send(null);
+ });
+}
+
+/**
+ * Downloads an XML document from a URL optionally testing the SSL certificate
+ * for certain attributes.
+ * NOTE - based on ProductAddonChecker.jsm
+ *
+ * @param url
+ * The url to download from.
+ * @param allowNonBuiltIn
+ * Whether to trust SSL certificates without a built-in CA issuer.
+ * @param allowedCerts
+ * The list of certificate attributes to match the SSL certificate
+ * against or null to skip checks.
+ * @return a promise that resolves to the DOM document downloaded or rejects
+ * with a JS exception in case of error.
+ */
+function downloadXML(url, allowNonBuiltIn = false, allowedCerts = null) {
+ return new Promise((resolve, reject) => {
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsISupports);
+ // This is here to let unit test code override XHR
+ if (request.wrappedJSObject) {
+ request = request.wrappedJSObject;
+ }
+ request.open("GET", url, true);
+ request.channel.notificationCallbacks = new BadCertHandler(allowNonBuiltIn);
+ // Prevent the request from reading from the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ request.timeout = TIMEOUT_DELAY_MS;
+
+ request.overrideMimeType("text/xml");
+ // The Cache-Control header is only interpreted by proxies and the
+ // final destination. It does not help if a resource is already
+ // cached locally.
+ request.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ request.setRequestHeader("Pragma", "no-cache");
+
+ let fail = (event) => {
+ request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading XML, status: " + status + ", reason: " + event.type;
+ //console.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+
+ let success = (event) => {
+ //console.info("Completed downloading document");
+ request = event.target;
+
+ try {
+ checkCert(request.channel, allowNonBuiltIn, allowedCerts);
+ } catch (ex) {
+ //console.error("Request failed certificate checks: " + ex);
+ ex.status = getRequestStatus(request);
+ reject(ex);
+ return;
+ }
+
+ resolve(request.responseXML);
+ };
+
+ request.addEventListener("error", fail, false);
+ request.addEventListener("abort", fail, false);
+ request.addEventListener("timeout", fail, false);
+ request.addEventListener("load", success, false);
+
+ //console.info("sending request to: " + url);
+ request.send(null);
+ });
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/install.rdf.in
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>diagnostics@mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+ <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+ <!-- Target Application this extension can install into,
+ with minimum and maximum supported versions. -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+ <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Diagnostics</em:name>
+ <em:description>Diagnose Firefox update problems.</em:description>
+ </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['diagnostics@mozilla.org'] += [
+ 'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['diagnostics@mozilla.org'] += [
+ 'install.rdf.in'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/browser/browser.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+[browser_check_installed.js]
+support-files =
+ diagnostics-data.xpi
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/browser/browser_check_installed.js
@@ -0,0 +1,39 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/TelemetryLog.jsm");
+
+add_task(function* test_enabled() {
+ let addon = yield new Promise(
+ resolve => AddonManager.getAddonByID("diagnostics@mozilla.org", resolve)
+ );
+ isnot(addon, null, "Check addon exists");
+ is(addon.version, "2.0", "Check version");
+ is(addon.name, "Diagnostics", "Check name");
+ ok(addon.isCompatible, "Check application compatibility");
+ ok(!addon.appDisabled, "Check not app disabled");
+ ok(addon.isActive, "Check addon is active");
+
+ let expected_results = [
+ "DIAGNOSTICS_ERROR_UNEXPECTED_UPDATE_URL",
+ "DIAGNOSTICS_SUCCESS_AUS_BUILT_IN_CERT",
+ "DIAGNOSTICS_SUCCESS_DOWNLOADED_SYSTEM_ADDON",
+ "DIAGNOSTICS_SUCCESS_SYSTEM_ADDON_CHECKSUM_MATCHES",
+ "DIAGNOSTICS_WARN_SYSTEM_ADDON_UPDATES_DIR_DOES_NOT_EXIST"
+ ];
+
+ while(true) {
+ let reasons = TelemetryLog.entries().map(entry => {
+ if (entry[0].startsWith("DIAGNOSTICS")) {
+ return entry[0];
+ }
+ });
+ if (expected_results.every(elem => reasons.includes(elem))) {
+ break;
+ }
+ }
+
+ delete window.TelemetryLog;
+});
new file mode 100644
index 0000000000000000000000000000000000000000..ffdcd26b46f4f9353ac03c54dbc03425fcc71a2e
GIT binary patch
literal 172
zc$^FHW@h1H0D;fdR$dH9fSW;vp|~_TIkmW0ucV?RG=!6Z`DR;J1PGT_a5FHnd}U-{
rU=aZ-4e(}Ul4HhYj0Bv$q!C1-7|IGU6vIGPHjq+AAPfZ3u3+r|tH>Qx
new file mode 100644
--- /dev/null
+++ b/browser/extensions/diagnostics/test/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['diagnostics@mozilla.org'] += [
+ 'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['diagnostics@mozilla.org'] += [
+ 'install.rdf.in'
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -1,16 +1,17 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DIRS += [
'aushelper',
+ 'diagnostics',
'disableSHA1rollout',
'e10srollout',
'pdfjs',
'pocket',
'webcompat',
'shield-recipe-client',
]
--- a/layout/tools/reftest/reftest-preferences.js
+++ b/layout/tools/reftest/reftest-preferences.js
@@ -18,16 +18,17 @@ user_pref("hangmonitor.timeout", 0);
user_pref("media.autoplay.enabled", true);
// Disable updates
user_pref("app.update.enabled", false);
user_pref("app.update.staging.enabled", false);
user_pref("app.update.url.android", "");
// Disable addon updates and prefetching so we don't leak them
user_pref("extensions.update.enabled", false);
user_pref("extensions.systemAddon.update.url", "http://localhost/dummy-system-addons.xml");
+user_pref("extensions.systemAddon.diagnostics.url", "http://localhost/dummy-system-addons-diagnostics/");
user_pref("extensions.getAddons.cache.enabled", false);
// Disable blocklist updates so we don't have them reported as leaks
user_pref("extensions.blocklist.enabled", false);
// Make url-classifier updates so rare that they won't affect tests
user_pref("urlclassifier.updateinterval", 172800);
// Disable downscale-during-decode, since it makes reftests more difficult.
user_pref("image.downscale-during-decode.enabled", false);
// Checking whether two files are the same is slow on Windows.
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -109,16 +109,17 @@ user_pref("privacy.trackingprotection.in
// Point update checks to the local testing server for fast failures
user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
user_pref("extensions.update.background.url", "http://%(server)s/extensions-dummy/updateBackgroundURL");
user_pref("extensions.blocklist.detailsURL", "http://%(server)s/extensions-dummy/blocklistDetailsURL");
user_pref("extensions.blocklist.itemURL", "http://%(server)s/extensions-dummy/blocklistItemURL");
user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
user_pref("extensions.systemAddon.update.url", "http://%(server)s/dummy-system-addons.xml");
+user_pref("extensions.systemAddon.diagnostics.url", "http://example.com/browser/browser/extensions/diagnostics/test/browser/");
// Turn off extension updates so they don't bother tests
user_pref("extensions.update.enabled", false);
// Make sure opening about:addons won't hit the network
user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
// Make sure AddonRepository won't hit the network
user_pref("extensions.getAddons.maxResults", 0);
user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/repositoryGetURL");
user_pref("extensions.getAddons.getWithPerformance.url", "http://%(server)s/extensions-dummy/repositoryGetWithPerformanceURL");
--- a/testing/talos/talos/config.py
+++ b/testing/talos/talos/config.py
@@ -142,16 +142,18 @@ DEFAULTS = dict(
'http://127.0.0.1/extensions-dummy/repositoryBrowseURL',
'extensions.getAddons.search.url':
'http://127.0.0.1/extensions-dummy/repositorySearchURL',
'media.gmp-manager.url':
'http://127.0.0.1/gmpmanager-dummy/update.xml',
'media.gmp-manager.updateEnabled': False,
'extensions.systemAddon.update.url':
'http://127.0.0.1/dummy-system-addons.xml',
+ 'extensions.systemAddon.diagnostics.url':
+ 'http://127.0.0.1/dummy-system-addons-diagnostics/',
'extensions.shield-recipe-client.api_url':
'https://127.0.0.1/selfsupport-dummy/',
'media.navigator.enabled': True,
'media.peerconnection.enabled': True,
'media.navigator.permission.disabled': True,
'media.capturestream_hints.enabled': True,
'browser.contentHandlers.types.0.uri': 'http://127.0.0.1/rss?url=%s',
'browser.contentHandlers.types.1.uri': 'http://127.0.0.1/rss?url=%s',