services/healthreport/healthreporter.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 #ifndef MERGED_COMPARTMENT
     9 this.EXPORTED_SYMBOLS = ["HealthReporter"];
    11 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    13 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
    15 Cu.import("resource://gre/modules/Metrics.jsm");
    16 Cu.import("resource://services-common/async.js");
    18 Cu.import("resource://services-common/bagheeraclient.js");
    19 #endif
    21 Cu.import("resource://gre/modules/Log.jsm");
    22 Cu.import("resource://services-common/utils.js");
    23 Cu.import("resource://gre/modules/Promise.jsm");
    24 Cu.import("resource://gre/modules/osfile.jsm");
    25 Cu.import("resource://gre/modules/Preferences.jsm");
    26 Cu.import("resource://gre/modules/Services.jsm");
    27 Cu.import("resource://gre/modules/Task.jsm");
    28 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
    29 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    31 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
    32                                   "resource://gre/modules/UpdateChannel.jsm");
    34 // Oldest year to allow in date preferences. This module was implemented in
    35 // 2012 and no dates older than that should be encountered.
    36 const OLDEST_ALLOWED_YEAR = 2012;
    38 const DAYS_IN_PAYLOAD = 180;
    40 const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
    42 const TELEMETRY_INIT = "HEALTHREPORT_INIT_MS";
    43 const TELEMETRY_INIT_FIRSTRUN = "HEALTHREPORT_INIT_FIRSTRUN_MS";
    44 const TELEMETRY_DB_OPEN = "HEALTHREPORT_DB_OPEN_MS";
    45 const TELEMETRY_DB_OPEN_FIRSTRUN = "HEALTHREPORT_DB_OPEN_FIRSTRUN_MS";
    46 const TELEMETRY_GENERATE_PAYLOAD = "HEALTHREPORT_GENERATE_JSON_PAYLOAD_MS";
    47 const TELEMETRY_JSON_PAYLOAD_SERIALIZE = "HEALTHREPORT_JSON_PAYLOAD_SERIALIZE_MS";
    48 const TELEMETRY_PAYLOAD_SIZE_UNCOMPRESSED = "HEALTHREPORT_PAYLOAD_UNCOMPRESSED_BYTES";
    49 const TELEMETRY_PAYLOAD_SIZE_COMPRESSED = "HEALTHREPORT_PAYLOAD_COMPRESSED_BYTES";
    50 const TELEMETRY_UPLOAD = "HEALTHREPORT_UPLOAD_MS";
    51 const TELEMETRY_SHUTDOWN_DELAY = "HEALTHREPORT_SHUTDOWN_DELAY_MS";
    52 const TELEMETRY_COLLECT_CONSTANT = "HEALTHREPORT_COLLECT_CONSTANT_DATA_MS";
    53 const TELEMETRY_COLLECT_DAILY = "HEALTHREPORT_COLLECT_DAILY_MS";
    54 const TELEMETRY_SHUTDOWN = "HEALTHREPORT_SHUTDOWN_MS";
    55 const TELEMETRY_COLLECT_CHECKPOINT = "HEALTHREPORT_POST_COLLECT_CHECKPOINT_MS";
    58 /**
    59  * Helper type to assist with management of Health Reporter state.
    60  *
    61  * Instances are not meant to be created outside of a HealthReporter instance.
    62  *
    63  * There are two types of IDs associated with clients.
    64  *
    65  * Since the beginning of FHR, there has existed a per-upload ID: a UUID is
    66  * generated at upload time and associated with the state before upload starts.
    67  * That same upload includes a request to delete all other upload IDs known by
    68  * the client.
    69  *
    70  * Per-upload IDs had the unintended side-effect of creating "orphaned"
    71  * records/upload IDs on the server. So, a stable client identifer has been
    72  * introduced. This client identifier is generated when it's missing and sent
    73  * as part of every upload.
    74  *
    75  * There is a high chance we may remove upload IDs in the future.
    76  */
    77 function HealthReporterState(reporter) {
    78   this._reporter = reporter;
    80   let profD = OS.Constants.Path.profileDir;
    82   if (!profD || !profD.length) {
    83     throw new Error("Could not obtain profile directory. OS.File not " +
    84                     "initialized properly?");
    85   }
    87   this._log = reporter._log;
    89   this._stateDir = OS.Path.join(profD, "healthreport");
    91   // To facilitate testing.
    92   let leaf = reporter._stateLeaf || "state.json";
    94   this._filename = OS.Path.join(this._stateDir, leaf);
    95   this._log.debug("Storing state in " + this._filename);
    96   this._s = null;
    97 }
    99 HealthReporterState.prototype = Object.freeze({
   100   /**
   101    * Persistent string identifier associated with this client.
   102    */
   103   get clientID() {
   104     return this._s.clientID;
   105   },
   107   /**
   108    * The version associated with the client ID.
   109    */
   110   get clientIDVersion() {
   111     return this._s.clientIDVersion;
   112   },
   114   get lastPingDate() {
   115     return new Date(this._s.lastPingTime);
   116   },
   118   get lastSubmitID() {
   119     return this._s.remoteIDs[0];
   120   },
   122   get remoteIDs() {
   123     return this._s.remoteIDs;
   124   },
   126   get _lastPayloadPath() {
   127     return OS.Path.join(this._stateDir, "lastpayload.json");
   128   },
   130   init: function () {
   131     return Task.spawn(function init() {
   132       try {
   133         OS.File.makeDir(this._stateDir);
   134       } catch (ex if ex instanceof OS.FileError) {
   135         if (!ex.becauseExists) {
   136           throw ex;
   137         }
   138       }
   140       let resetObjectState = function () {
   141         this._s = {
   142           // The payload version. This is bumped whenever there is a
   143           // backwards-incompatible change.
   144           v: 1,
   145           // The persistent client identifier.
   146           clientID: CommonUtils.generateUUID(),
   147           // Denotes the mechanism used to generate the client identifier.
   148           // 1: Random UUID.
   149           clientIDVersion: 1,
   150           // Upload IDs that might be on the server.
   151           remoteIDs: [],
   152           // When we last performed an uploaded.
   153           lastPingTime: 0,
   154           // Tracks whether we removed an outdated payload.
   155           removedOutdatedLastpayload: false,
   156         };
   157       }.bind(this);
   159       try {
   160         this._s = yield CommonUtils.readJSON(this._filename);
   161       } catch (ex if ex instanceof OS.File.Error) {
   162         if (!ex.becauseNoSuchFile) {
   163           throw ex;
   164         }
   166         this._log.warn("Saved state file does not exist.");
   167         resetObjectState();
   168       } catch (ex) {
   169         this._log.error("Exception when reading state from disk: " +
   170                         CommonUtils.exceptionStr(ex));
   171         resetObjectState();
   173         // Don't save in case it goes away on next run.
   174       }
   176       if (typeof(this._s) != "object") {
   177         this._log.warn("Read state is not an object. Resetting state.");
   178         resetObjectState();
   179         yield this.save();
   180       }
   182       if (this._s.v != 1) {
   183         this._log.warn("Unknown version in state file: " + this._s.v);
   184         resetObjectState();
   185         // We explicitly don't save here in the hopes an application re-upgrade
   186         // comes along and fixes us.
   187       }
   189       let regen = false;
   190       if (!this._s.clientID) {
   191         this._log.warn("No client ID stored. Generating random ID.");
   192         regen = true;
   193       }
   195       if (typeof(this._s.clientID) != "string") {
   196         this._log.warn("Client ID is not a string. Regenerating.");
   197         regen = true;
   198       }
   200       if (regen) {
   201         this._s.clientID = CommonUtils.generateUUID();
   202         this._s.clientIDVersion = 1;
   203         yield this.save();
   204       }
   206       // Always look for preferences. This ensures that downgrades followed
   207       // by reupgrades don't result in excessive data loss.
   208       for (let promise of this._migratePrefs()) {
   209         yield promise;
   210       }
   211     }.bind(this));
   212   },
   214   save: function () {
   215     this._log.info("Writing state file: " + this._filename);
   216     return CommonUtils.writeJSON(this._s, this._filename);
   217   },
   219   addRemoteID: function (id) {
   220     this._log.warn("Recording new remote ID: " + id);
   221     this._s.remoteIDs.push(id);
   222     return this.save();
   223   },
   225   removeRemoteID: function (id) {
   226     return this.removeRemoteIDs(id ? [id] : []);
   227   },
   229   removeRemoteIDs: function (ids) {
   230     if (!ids || !ids.length) {
   231       this._log.warn("No IDs passed for removal.");
   232       return Promise.resolve();
   233     }
   235     this._log.warn("Removing documents from remote ID list: " + ids);
   236     let filtered = this._s.remoteIDs.filter((x) => ids.indexOf(x) === -1);
   238     if (filtered.length == this._s.remoteIDs.length) {
   239       return Promise.resolve();
   240     }
   242     this._s.remoteIDs = filtered;
   243     return this.save();
   244   },
   246   setLastPingDate: function (date) {
   247     this._s.lastPingTime = date.getTime();
   249     return this.save();
   250   },
   252   updateLastPingAndRemoveRemoteID: function (date, id) {
   253     return this.updateLastPingAndRemoveRemoteIDs(date, id ? [id] : []);
   254   },
   256   updateLastPingAndRemoveRemoteIDs: function (date, ids) {
   257     if (!ids) {
   258       return this.setLastPingDate(date);
   259     }
   261     this._log.info("Recording last ping time and deleted remote document.");
   262     this._s.lastPingTime = date.getTime();
   263     return this.removeRemoteIDs(ids);
   264   },
   266   /**
   267    * Reset the client ID to something else.
   268    *
   269    * This fails if remote IDs are stored because changing the client ID
   270    * while there is remote data will create orphaned records.
   271    */
   272   resetClientID: function () {
   273     if (this.remoteIDs.length) {
   274       throw new Error("Cannot reset client ID while remote IDs are stored.");
   275     }
   277     this._log.warn("Resetting client ID.");
   278     this._s.clientID = CommonUtils.generateUUID();
   279     this._s.clientIDVersion = 1;
   281     return this.save();
   282   },
   284   _migratePrefs: function () {
   285     let prefs = this._reporter._prefs;
   287     let lastID = prefs.get("lastSubmitID", null);
   288     let lastPingDate = CommonUtils.getDatePref(prefs, "lastPingTime",
   289                                                0, this._log, OLDEST_ALLOWED_YEAR);
   291     // If we have state from prefs, migrate and save it to a file then clear
   292     // out old prefs.
   293     if (lastID || (lastPingDate && lastPingDate.getTime() > 0)) {
   294       this._log.warn("Migrating saved state from preferences.");
   296       if (lastID) {
   297         this._log.info("Migrating last saved ID: " + lastID);
   298         this._s.remoteIDs.push(lastID);
   299       }
   301       let ourLast = this.lastPingDate;
   303       if (lastPingDate && lastPingDate.getTime() > ourLast.getTime()) {
   304         this._log.info("Migrating last ping time: " + lastPingDate);
   305         this._s.lastPingTime = lastPingDate.getTime();
   306       }
   308       yield this.save();
   309       prefs.reset(["lastSubmitID", "lastPingTime"]);
   310     } else {
   311       this._log.warn("No prefs data found.");
   312     }
   313   },
   314 });
   316 /**
   317  * This is the abstract base class of `HealthReporter`. It exists so that
   318  * we can sanely divide work on platforms where control of Firefox Health
   319  * Report is outside of Gecko (e.g., Android).
   320  */
   321 function AbstractHealthReporter(branch, policy, sessionRecorder) {
   322   if (!branch.endsWith(".")) {
   323     throw new Error("Branch must end with a period (.): " + branch);
   324   }
   326   if (!policy) {
   327     throw new Error("Must provide policy to HealthReporter constructor.");
   328   }
   330   this._log = Log.repository.getLogger("Services.HealthReport.HealthReporter");
   331   this._log.info("Initializing health reporter instance against " + branch);
   333   this._branch = branch;
   334   this._prefs = new Preferences(branch);
   336   this._policy = policy;
   337   this.sessionRecorder = sessionRecorder;
   339   this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME;
   341   this._storage = null;
   342   this._storageInProgress = false;
   343   this._providerManager = null;
   344   this._providerManagerInProgress = false;
   345   this._initializeStarted = false;
   346   this._initialized = false;
   347   this._initializeHadError = false;
   348   this._initializedDeferred = Promise.defer();
   349   this._shutdownRequested = false;
   350   this._shutdownInitiated = false;
   351   this._shutdownComplete = false;
   352   this._shutdownCompleteCallback = null;
   354   this._errors = [];
   356   this._lastDailyDate = null;
   358   // Yes, this will probably run concurrently with remaining constructor work.
   359   let hasFirstRun = this._prefs.get("service.firstRun", false);
   360   this._initHistogram = hasFirstRun ? TELEMETRY_INIT : TELEMETRY_INIT_FIRSTRUN;
   361   this._dbOpenHistogram = hasFirstRun ? TELEMETRY_DB_OPEN : TELEMETRY_DB_OPEN_FIRSTRUN;
   362 }
   364 AbstractHealthReporter.prototype = Object.freeze({
   365   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
   367   /**
   368    * Whether the service is fully initialized and running.
   369    *
   370    * If this is false, it is not safe to call most functions.
   371    */
   372   get initialized() {
   373     return this._initialized;
   374   },
   376   /**
   377    * Initialize the instance.
   378    *
   379    * This must be called once after object construction or the instance is
   380    * useless.
   381    */
   382   init: function () {
   383     if (this._initializeStarted) {
   384       throw new Error("We have already started initialization.");
   385     }
   387     this._initializeStarted = true;
   389     TelemetryStopwatch.start(this._initHistogram, this);
   391     this._initializeState().then(this._onStateInitialized.bind(this),
   392                                  this._onInitError.bind(this));
   394     return this.onInit();
   395   },
   397   //----------------------------------------------------
   398   // SERVICE CONTROL FUNCTIONS
   399   //
   400   // You shouldn't need to call any of these externally.
   401   //----------------------------------------------------
   403   _onInitError: function (error) {
   404     TelemetryStopwatch.cancel(this._initHistogram, this);
   405     TelemetryStopwatch.cancel(this._dbOpenHistogram, this);
   406     delete this._initHistogram;
   407     delete this._dbOpenHistogram;
   409     this._recordError("Error during initialization", error);
   410     this._initializeHadError = true;
   411     this._initiateShutdown();
   412     this._initializedDeferred.reject(error);
   414     // FUTURE consider poisoning prototype's functions so calls fail with a
   415     // useful error message.
   416   },
   418   _initializeState: function () {
   419     return Promise.resolve();
   420   },
   422   _onStateInitialized: function () {
   423     return Task.spawn(function onStateInitialized () {
   424       try {
   425         if (!this._state._s.removedOutdatedLastpayload) {
   426           yield this._deleteOldLastPayload();
   427           this._state._s.removedOutdatedLastpayload = true;
   428           // Normally we should save this to a file but it directly conflicts with
   429           //  the "application re-upgrade" decision in HealthReporterState::init()
   430           //  which specifically does not save the state to a file.
   431         }
   432       } catch (ex) {
   433         this._log.error("Error deleting last payload: " +
   434                         CommonUtils.exceptionStr(ex));
   435       }
   436       // As soon as we have could storage, we need to register cleanup or
   437       // else bad things happen on shutdown.
   438       Services.obs.addObserver(this, "quit-application", false);
   439       Services.obs.addObserver(this, "profile-before-change", false);
   441       this._storageInProgress = true;
   442       TelemetryStopwatch.start(this._dbOpenHistogram, this);
   444       Metrics.Storage(this._dbName).then(this._onStorageCreated.bind(this),
   445                                          this._onInitError.bind(this));
   446     }.bind(this));
   447   },
   450   /**
   451    * Removes the outdated lastpaylaod.json and lastpayload.json.tmp files
   452    * @see Bug #867902
   453    * @return a promise for when all the files have been deleted
   454    */
   455   _deleteOldLastPayload: function () {
   456     let paths = [this._state._lastPayloadPath, this._state._lastPayloadPath + ".tmp"];
   457     return Task.spawn(function removeAllFiles () {
   458       for (let path of paths) {
   459         try {
   460           OS.File.remove(path);
   461         } catch (ex) {
   462           if (!ex.becauseNoSuchFile) {
   463             this._log.error("Exception when removing outdated payload files: " +
   464                             CommonUtils.exceptionStr(ex));
   465           }
   466         }
   467       }
   468     }.bind(this));
   469   },
   471   // Called when storage has been opened.
   472   _onStorageCreated: function (storage) {
   473     TelemetryStopwatch.finish(this._dbOpenHistogram, this);
   474     delete this._dbOpenHistogram;
   475     this._log.info("Storage initialized.");
   476     this._storage = storage;
   477     this._storageInProgress = false;
   479     if (this._shutdownRequested) {
   480       this._initiateShutdown();
   481       return;
   482     }
   484     Task.spawn(this._initializeProviderManager.bind(this))
   485         .then(this._onProviderManagerInitialized.bind(this),
   486               this._onInitError.bind(this));
   487   },
   489   _initializeProviderManager: function () {
   490     if (this._collector) {
   491       throw new Error("Provider manager has already been initialized.");
   492     }
   494     this._log.info("Initializing provider manager.");
   495     this._providerManager = new Metrics.ProviderManager(this._storage);
   496     this._providerManager.onProviderError = this._recordError.bind(this);
   497     this._providerManager.onProviderInit = this._initProvider.bind(this);
   498     this._providerManagerInProgress = true;
   500     let catString = this._prefs.get("service.providerCategories") || "";
   501     if (catString.length) {
   502       for (let category of catString.split(",")) {
   503         yield this._providerManager.registerProvidersFromCategoryManager(category);
   504       }
   505     }
   506   },
   508   _onProviderManagerInitialized: function () {
   509     TelemetryStopwatch.finish(this._initHistogram, this);
   510     delete this._initHistogram;
   511     this._log.debug("Provider manager initialized.");
   512     this._providerManagerInProgress = false;
   514     if (this._shutdownRequested) {
   515       this._initiateShutdown();
   516       return;
   517     }
   519     this._log.info("HealthReporter started.");
   520     this._initialized = true;
   521     Services.obs.addObserver(this, "idle-daily", false);
   523     // If upload is not enabled, ensure daily collection works. If upload
   524     // is enabled, this will be performed as part of upload.
   525     //
   526     // This is important because it ensures about:healthreport contains
   527     // longitudinal data even if upload is disabled. Having about:healthreport
   528     // provide useful info even if upload is disabled was a core launch
   529     // requirement.
   530     //
   531     // We do not catch changes to the backing pref. So, if the session lasts
   532     // many days, we may fail to collect. However, most sessions are short and
   533     // this code will likely be refactored as part of splitting up policy to
   534     // serve Android. So, meh.
   535     if (!this._policy.healthReportUploadEnabled) {
   536       this._log.info("Upload not enabled. Scheduling daily collection.");
   537       // Since the timer manager is a singleton and there could be multiple
   538       // HealthReporter instances, we need to encode a unique identifier in
   539       // the timer ID.
   540       try {
   541         let timerName = this._branch.replace(".", "-", "g") + "lastDailyCollection";
   542         let tm = Cc["@mozilla.org/updates/timer-manager;1"]
   543                    .getService(Ci.nsIUpdateTimerManager);
   544         tm.registerTimer(timerName, this.collectMeasurements.bind(this),
   545                          24 * 60 * 60);
   546       } catch (ex) {
   547         this._log.error("Error registering collection timer: " +
   548                         CommonUtils.exceptionStr(ex));
   549       }
   550     }
   552     // Clean up caches and reduce memory usage.
   553     this._storage.compact();
   554     this._initializedDeferred.resolve(this);
   555   },
   557   // nsIObserver to handle shutdown.
   558   observe: function (subject, topic, data) {
   559     switch (topic) {
   560       case "quit-application":
   561         Services.obs.removeObserver(this, "quit-application");
   562         this._initiateShutdown();
   563         break;
   565       case "profile-before-change":
   566         Services.obs.removeObserver(this, "profile-before-change");
   567         this._waitForShutdown();
   568         break;
   570       case "idle-daily":
   571         this._performDailyMaintenance();
   572         break;
   573     }
   574   },
   576   _initiateShutdown: function () {
   577     // Ensure we only begin the main shutdown sequence once.
   578     if (this._shutdownInitiated) {
   579       this._log.warn("Shutdown has already been initiated. No-op.");
   580       return;
   581     }
   583     this._log.info("Request to shut down.");
   585     this._initialized = false;
   586     this._shutdownRequested = true;
   588     if (this._initializeHadError) {
   589       this._log.warn("Initialization had error. Shutting down immediately.");
   590     } else {
   591       if (this._providerManagerInProcess) {
   592         this._log.warn("Provider manager is in progress of initializing. " +
   593                        "Waiting to finish.");
   594         return;
   595       }
   597       // If storage is in the process of initializing, we need to wait for it
   598       // to finish before continuing. The initialization process will call us
   599       // again once storage has initialized.
   600       if (this._storageInProgress) {
   601         this._log.warn("Storage is in progress of initializing. Waiting to finish.");
   602         return;
   603       }
   604     }
   606     this._log.warn("Initiating main shutdown procedure.");
   608     // Everything from here must only be performed once or else race conditions
   609     // could occur.
   611     TelemetryStopwatch.start(TELEMETRY_SHUTDOWN, this);
   612     this._shutdownInitiated = true;
   614     // We may not have registered the observer yet. If not, this will
   615     // throw.
   616     try {
   617       Services.obs.removeObserver(this, "idle-daily");
   618     } catch (ex) { }
   620     if (this._providerManager) {
   621       let onShutdown = this._onProviderManagerShutdown.bind(this);
   622       Task.spawn(this._shutdownProviderManager.bind(this))
   623           .then(onShutdown, onShutdown);
   624       return;
   625     }
   627     this._log.warn("Don't have provider manager. Proceeding to storage shutdown.");
   628     this._shutdownStorage();
   629   },
   631   _shutdownProviderManager: function () {
   632     this._log.info("Shutting down provider manager.");
   633     for (let provider of this._providerManager.providers) {
   634       try {
   635         yield provider.shutdown();
   636       } catch (ex) {
   637         this._log.warn("Error when shutting down provider: " +
   638                        CommonUtils.exceptionStr(ex));
   639       }
   640     }
   641   },
   643   _onProviderManagerShutdown: function () {
   644     this._log.info("Provider manager shut down.");
   645     this._providerManager = null;
   646     this._shutdownStorage();
   647   },
   649   _shutdownStorage: function () {
   650     if (!this._storage) {
   651       this._onShutdownComplete();
   652     }
   654     this._log.info("Shutting down storage.");
   655     let onClose = this._onStorageClose.bind(this);
   656     this._storage.close().then(onClose, onClose);
   657   },
   659   _onStorageClose: function (error) {
   660     this._log.info("Storage has been closed.");
   662     if (error) {
   663       this._log.warn("Error when closing storage: " +
   664                      CommonUtils.exceptionStr(error));
   665     }
   667     this._storage = null;
   668     this._onShutdownComplete();
   669   },
   671   _onShutdownComplete: function () {
   672     this._log.warn("Shutdown complete.");
   673     this._shutdownComplete = true;
   674     TelemetryStopwatch.finish(TELEMETRY_SHUTDOWN, this);
   676     if (this._shutdownCompleteCallback) {
   677       this._shutdownCompleteCallback();
   678     }
   679   },
   681   _waitForShutdown: function () {
   682     if (this._shutdownComplete) {
   683       return;
   684     }
   686     TelemetryStopwatch.start(TELEMETRY_SHUTDOWN_DELAY, this);
   687     try {
   688       this._shutdownCompleteCallback = Async.makeSpinningCallback();
   689       this._shutdownCompleteCallback.wait();
   690       this._shutdownCompleteCallback = null;
   691     } finally {
   692       TelemetryStopwatch.finish(TELEMETRY_SHUTDOWN_DELAY, this);
   693     }
   694   },
   696   /**
   697    * Convenience method to shut down the instance.
   698    *
   699    * This should *not* be called outside of tests.
   700    */
   701   _shutdown: function () {
   702     this._initiateShutdown();
   703     this._waitForShutdown();
   704   },
   706   /**
   707    * Return a promise that is resolved once the service has been initialized.
   708    */
   709   onInit: function () {
   710     if (this._initializeHadError) {
   711       throw new Error("Service failed to initialize.");
   712     }
   714     if (this._initialized) {
   715       return CommonUtils.laterTickResolvingPromise(this);
   716     }
   718     return this._initializedDeferred.promise;
   719   },
   721   _performDailyMaintenance: function () {
   722     this._log.info("Request to perform daily maintenance.");
   724     if (!this._initialized) {
   725       return;
   726     }
   728     let now = new Date();
   729     let cutoff = new Date(now.getTime() - MILLISECONDS_PER_DAY * (DAYS_IN_PAYLOAD - 1));
   731     // The operation is enqueued and put in a transaction by the storage module.
   732     this._storage.pruneDataBefore(cutoff);
   733   },
   735   //--------------------
   736   // Provider Management
   737   //--------------------
   739   /**
   740    * Obtain a provider from its name.
   741    *
   742    * This will only return providers that are currently initialized. If
   743    * a provider is lazy initialized (like pull-only providers) this
   744    * will likely not return anything.
   745    */
   746   getProvider: function (name) {
   747     if (!this._providerManager) {
   748       return null;
   749     }
   751     return this._providerManager.getProvider(name);
   752   },
   754   _initProvider: function (provider) {
   755     provider.healthReporter = this;
   756   },
   758   /**
   759    * Record an exception for reporting in the payload.
   760    *
   761    * A side effect is the exception is logged.
   762    *
   763    * Note that callers need to be extra sensitive about ensuring personal
   764    * or otherwise private details do not leak into this. All of the user data
   765    * on the stack in FHR code should be limited to data we were collecting with
   766    * the intent to submit. So, it is covered under the user's consent to use
   767    * the feature.
   768    *
   769    * @param message
   770    *        (string) Human readable message describing error.
   771    * @param ex
   772    *        (Error) The error that should be captured.
   773    */
   774   _recordError: function (message, ex) {
   775     let recordMessage = message;
   776     let logMessage = message;
   778     if (ex) {
   779       recordMessage += ": " + CommonUtils.exceptionStr(ex);
   780       logMessage += ": " + CommonUtils.exceptionStr(ex);
   781     }
   783     // Scrub out potentially identifying information from strings that could
   784     // make the payload.
   785     let appData = Services.dirsvc.get("UAppData", Ci.nsIFile);
   786     let profile = Services.dirsvc.get("ProfD", Ci.nsIFile);
   788     let appDataURI = Services.io.newFileURI(appData);
   789     let profileURI = Services.io.newFileURI(profile);
   791     // Order of operation is important here. We do the URI before the path version
   792     // because the path may be a subset of the URI. We also have to check for the case
   793     // where UAppData is underneath the profile directory (or vice-versa) so we
   794     // don't substitute incomplete strings.
   796     function replace(uri, path, thing) {
   797       // Try is because .spec can throw on invalid URI.
   798       try {
   799         recordMessage = recordMessage.replace(uri.spec, '<' + thing + 'URI>', 'g');
   800       } catch (ex) { }
   802       recordMessage = recordMessage.replace(path, '<' + thing + 'Path>', 'g');
   803     }
   805     if (appData.path.contains(profile.path)) {
   806       replace(appDataURI, appData.path, 'AppData');
   807       replace(profileURI, profile.path, 'Profile');
   808     } else {
   809       replace(profileURI, profile.path, 'Profile');
   810       replace(appDataURI, appData.path, 'AppData');
   811     }
   813     this._log.warn(logMessage);
   814     this._errors.push(recordMessage);
   815   },
   817   /**
   818    * Collect all measurements for all registered providers.
   819    */
   820   collectMeasurements: function () {
   821     if (!this._initialized) {
   822       return Promise.reject(new Error("Not initialized."));
   823     }
   825     return Task.spawn(function doCollection() {
   826       yield this._providerManager.ensurePullOnlyProvidersRegistered();
   828       try {
   829         TelemetryStopwatch.start(TELEMETRY_COLLECT_CONSTANT, this);
   830         yield this._providerManager.collectConstantData();
   831         TelemetryStopwatch.finish(TELEMETRY_COLLECT_CONSTANT, this);
   832       } catch (ex) {
   833         TelemetryStopwatch.cancel(TELEMETRY_COLLECT_CONSTANT, this);
   834         this._log.warn("Error collecting constant data: " +
   835                        CommonUtils.exceptionStr(ex));
   836       }
   838       // Daily data is collected if it hasn't yet been collected this
   839       // application session or if it has been more than a day since the
   840       // last collection. This means that providers could see many calls to
   841       // collectDailyData per calendar day. However, this collection API
   842       // makes no guarantees about limits. The alternative would involve
   843       // recording state. The simpler implementation prevails for now.
   844       if (!this._lastDailyDate ||
   845           Date.now() - this._lastDailyDate > MILLISECONDS_PER_DAY) {
   847         try {
   848           TelemetryStopwatch.start(TELEMETRY_COLLECT_DAILY, this);
   849           this._lastDailyDate = new Date();
   850           yield this._providerManager.collectDailyData();
   851           TelemetryStopwatch.finish(TELEMETRY_COLLECT_DAILY, this);
   852         } catch (ex) {
   853           TelemetryStopwatch.cancel(TELEMETRY_COLLECT_DAILY, this);
   854           this._log.warn("Error collecting daily data from providers: " +
   855                          CommonUtils.exceptionStr(ex));
   856         }
   857       }
   859       yield this._providerManager.ensurePullOnlyProvidersUnregistered();
   861       // Flush gathered data to disk. This will incur an fsync. But, if
   862       // there is ever a time we want to persist data to disk, it's
   863       // after a massive collection.
   864       try {
   865         TelemetryStopwatch.start(TELEMETRY_COLLECT_CHECKPOINT, this);
   866         yield this._storage.checkpoint();
   867         TelemetryStopwatch.finish(TELEMETRY_COLLECT_CHECKPOINT, this);
   868       } catch (ex) {
   869         TelemetryStopwatch.cancel(TELEMETRY_COLLECT_CHECKPOINT, this);
   870         throw ex;
   871       }
   873       throw new Task.Result();
   874     }.bind(this));
   875   },
   877   /**
   878    * Helper function to perform data collection and obtain the JSON payload.
   879    *
   880    * If you are looking for an up-to-date snapshot of FHR data that pulls in
   881    * new data since the last upload, this is how you should obtain it.
   882    *
   883    * @param asObject
   884    *        (bool) Whether to resolve an object or JSON-encoded string of that
   885    *        object (the default).
   886    *
   887    * @return Promise<Object | string>
   888    */
   889   collectAndObtainJSONPayload: function (asObject=false) {
   890     if (!this._initialized) {
   891       return Promise.reject(new Error("Not initialized."));
   892     }
   894     return Task.spawn(function collectAndObtain() {
   895       yield this._storage.setAutoCheckpoint(0);
   896       yield this._providerManager.ensurePullOnlyProvidersRegistered();
   898       let payload;
   899       let error;
   901       try {
   902         yield this.collectMeasurements();
   903         payload = yield this.getJSONPayload(asObject);
   904       } catch (ex) {
   905         error = ex;
   906         this._collectException("Error collecting and/or retrieving JSON payload",
   907                                ex);
   908       } finally {
   909         yield this._providerManager.ensurePullOnlyProvidersUnregistered();
   910         yield this._storage.setAutoCheckpoint(1);
   912         if (error) {
   913           throw error;
   914         }
   915       }
   917       // We hold off throwing to ensure that behavior between finally
   918       // and generators and throwing is sane.
   919       throw new Task.Result(payload);
   920     }.bind(this));
   921   },
   924   /**
   925    * Obtain the JSON payload for currently-collected data.
   926    *
   927    * The payload only contains data that has been recorded to FHR. Some
   928    * providers may have newer data available. If you want to ensure you
   929    * have all available data, call `collectAndObtainJSONPayload`
   930    * instead.
   931    *
   932    * @param asObject
   933    *        (bool) Whether to return an object or JSON encoding of that
   934    *        object (the default).
   935    *
   936    * @return Promise<string|object>
   937    */
   938   getJSONPayload: function (asObject=false) {
   939     TelemetryStopwatch.start(TELEMETRY_GENERATE_PAYLOAD, this);
   940     let deferred = Promise.defer();
   942     Task.spawn(this._getJSONPayload.bind(this, this._now(), asObject)).then(
   943       function onResult(result) {
   944         TelemetryStopwatch.finish(TELEMETRY_GENERATE_PAYLOAD, this);
   945         deferred.resolve(result);
   946       }.bind(this),
   947       function onError(error) {
   948         TelemetryStopwatch.cancel(TELEMETRY_GENERATE_PAYLOAD, this);
   949         deferred.reject(error);
   950       }.bind(this)
   951     );
   953     return deferred.promise;
   954   },
   956   _getJSONPayload: function (now, asObject=false) {
   957     let pingDateString = this._formatDate(now);
   958     this._log.info("Producing JSON payload for " + pingDateString);
   960     // May not be present if we are generating as a result of init error.
   961     if (this._providerManager) {
   962       yield this._providerManager.ensurePullOnlyProvidersRegistered();
   963     }
   965     let o = {
   966       version: 2,
   967       clientID: this._state.clientID,
   968       clientIDVersion: this._state.clientIDVersion,
   969       thisPingDate: pingDateString,
   970       geckoAppInfo: this.obtainAppInfo(this._log),
   971       data: {last: {}, days: {}},
   972     };
   974     let outputDataDays = o.data.days;
   976     // Guard here in case we don't track this (e.g., on Android).
   977     let lastPingDate = this.lastPingDate;
   978     if (lastPingDate && lastPingDate.getTime() > 0) {
   979       o.lastPingDate = this._formatDate(lastPingDate);
   980     }
   982     // We can still generate a payload even if we're not initialized.
   983     // This is to facilitate error upload on init failure.
   984     if (this._initialized) {
   985       for (let provider of this._providerManager.providers) {
   986         let providerName = provider.name;
   988         let providerEntry = {
   989           measurements: {},
   990         };
   992         // Measurement name to recorded version.
   993         let lastVersions = {};
   994         // Day string to mapping of measurement name to recorded version.
   995         let dayVersions = {};
   997         for (let [measurementKey, measurement] of provider.measurements) {
   998           let name = providerName + "." + measurement.name;
   999           let version = measurement.version;
  1001           let serializer;
  1002           try {
  1003             // The measurement is responsible for returning a serializer which
  1004             // is aware of the measurement version.
  1005             serializer = measurement.serializer(measurement.SERIALIZE_JSON);
  1006           } catch (ex) {
  1007             this._recordError("Error obtaining serializer for measurement: " +
  1008                               name, ex);
  1009             continue;
  1012           let data;
  1013           try {
  1014             data = yield measurement.getValues();
  1015           } catch (ex) {
  1016             this._recordError("Error obtaining data for measurement: " + name,
  1017                               ex);
  1018             continue;
  1021           if (data.singular.size) {
  1022             try {
  1023               let serialized = serializer.singular(data.singular);
  1024               if (serialized) {
  1025                 // Only replace the existing data if there is no data or if our
  1026                 // version is newer than the old one.
  1027                 if (!(name in o.data.last) || version > lastVersions[name]) {
  1028                   o.data.last[name] = serialized;
  1029                   lastVersions[name] = version;
  1032             } catch (ex) {
  1033               this._recordError("Error serializing singular data: " + name,
  1034                                 ex);
  1035               continue;
  1039           let dataDays = data.days;
  1040           for (let i = 0; i < DAYS_IN_PAYLOAD; i++) {
  1041             let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
  1042             if (!dataDays.hasDay(date)) {
  1043               continue;
  1045             let dateFormatted = this._formatDate(date);
  1047             try {
  1048               let serialized = serializer.daily(dataDays.getDay(date));
  1049               if (!serialized) {
  1050                 continue;
  1053               if (!(dateFormatted in outputDataDays)) {
  1054                 outputDataDays[dateFormatted] = {};
  1057               // This needs to be separate because dayVersions is provider
  1058               // specific and gets blown away in a loop while outputDataDays
  1059               // is persistent.
  1060               if (!(dateFormatted in dayVersions)) {
  1061                 dayVersions[dateFormatted] = {};
  1064               if (!(name in outputDataDays[dateFormatted]) ||
  1065                   version > dayVersions[dateFormatted][name]) {
  1066                 outputDataDays[dateFormatted][name] = serialized;
  1067                 dayVersions[dateFormatted][name] = version;
  1069             } catch (ex) {
  1070               this._recordError("Error populating data for day: " + name, ex);
  1071               continue;
  1076     } else {
  1077       o.notInitialized = 1;
  1078       this._log.warn("Not initialized. Sending report with only error info.");
  1081     if (this._errors.length) {
  1082       o.errors = this._errors.slice(0, 20);
  1085     if (this._initialized) {
  1086       this._storage.compact();
  1089     if (!asObject) {
  1090       TelemetryStopwatch.start(TELEMETRY_JSON_PAYLOAD_SERIALIZE, this);
  1091       o = JSON.stringify(o);
  1092       TelemetryStopwatch.finish(TELEMETRY_JSON_PAYLOAD_SERIALIZE, this);
  1095     if (this._providerManager) {
  1096       yield this._providerManager.ensurePullOnlyProvidersUnregistered();
  1099     throw new Task.Result(o);
  1100   },
  1102   _now: function _now() {
  1103     return new Date();
  1104   },
  1106   // These are stolen from AppInfoProvider.
  1107   appInfoVersion: 1,
  1108   appInfoFields: {
  1109     // From nsIXULAppInfo.
  1110     vendor: "vendor",
  1111     name: "name",
  1112     id: "ID",
  1113     version: "version",
  1114     appBuildID: "appBuildID",
  1115     platformVersion: "platformVersion",
  1116     platformBuildID: "platformBuildID",
  1118     // From nsIXULRuntime.
  1119     os: "OS",
  1120     xpcomabi: "XPCOMABI",
  1121   },
  1123   /**
  1124    * Statically return a bundle of app info data, a subset of that produced by
  1125    * AppInfoProvider._populateConstants. This allows us to more usefully handle
  1126    * payloads that, due to error, contain no data.
  1128    * Returns a very sparse object if Services.appinfo is unavailable.
  1129    */
  1130   obtainAppInfo: function () {
  1131     let out = {"_v": this.appInfoVersion};
  1132     try {
  1133       let ai = Services.appinfo;
  1134       for (let [k, v] in Iterator(this.appInfoFields)) {
  1135         out[k] = ai[v];
  1137     } catch (ex) {
  1138       this._log.warn("Could not obtain Services.appinfo: " +
  1139                      CommonUtils.exceptionStr(ex));
  1142     try {
  1143       out["updateChannel"] = UpdateChannel.get();
  1144     } catch (ex) {
  1145       this._log.warn("Could not obtain update channel: " +
  1146                      CommonUtils.exceptionStr(ex));
  1149     return out;
  1150   },
  1151 });
  1153 /**
  1154  * HealthReporter and its abstract superclass coordinate collection and
  1155  * submission of health report metrics.
  1157  * This is the main type for Firefox Health Report on desktop. It glues all the
  1158  * lower-level components (such as collection and submission) together.
  1160  * An instance of this type is created as an XPCOM service. See
  1161  * DataReportingService.js and
  1162  * DataReporting.manifest/HealthReportComponents.manifest.
  1164  * It is theoretically possible to have multiple instances of this running
  1165  * in the application. For example, this type may one day handle submission
  1166  * of telemetry data as well. However, there is some moderate coupling between
  1167  * this type and *the* Firefox Health Report (e.g., the policy). This could
  1168  * be abstracted if needed.
  1170  * Note that `AbstractHealthReporter` exists to allow for Firefox Health Report
  1171  * to be more easily implemented on platforms where a separate controlling
  1172  * layer is responsible for payload upload and deletion.
  1174  * IMPLEMENTATION NOTES
  1175  * ====================
  1177  * These notes apply to the combination of `HealthReporter` and
  1178  * `AbstractHealthReporter`.
  1180  * Initialization and shutdown are somewhat complicated and worth explaining
  1181  * in extra detail.
  1183  * The complexity is driven by the requirements of SQLite connection management.
  1184  * Once you have a SQLite connection, it isn't enough to just let the
  1185  * application shut down. If there is an open connection or if there are
  1186  * outstanding SQL statements come XPCOM shutdown time, Storage will assert.
  1187  * On debug builds you will crash. On release builds you will get a shutdown
  1188  * hang. This must be avoided!
  1190  * During initialization, the second we create a SQLite connection (via
  1191  * Metrics.Storage) we register observers for application shutdown. The
  1192  * "quit-application" notification initiates our shutdown procedure. The
  1193  * subsequent "profile-do-change" notification ensures it has completed.
  1195  * The handler for "profile-do-change" may result in event loop spinning. This
  1196  * is because of race conditions between our shutdown code and application
  1197  * shutdown.
  1199  * All of our shutdown routines are async. There is the potential that these
  1200  * async functions will not complete before XPCOM shutdown. If they don't
  1201  * finish in time, we could get assertions in Storage. Our solution is to
  1202  * initiate storage early in the shutdown cycle ("quit-application").
  1203  * Hopefully all the async operations have completed by the time we reach
  1204  * "profile-do-change." If so, great. If not, we spin the event loop until
  1205  * they have completed, avoiding potential race conditions.
  1207  * @param branch
  1208  *        (string) The preferences branch to use for state storage. The value
  1209  *        must end with a period (.).
  1211  * @param policy
  1212  *        (HealthReportPolicy) Policy driving execution of HealthReporter.
  1213  */
  1214 this.HealthReporter = function (branch, policy, sessionRecorder, stateLeaf=null) {
  1215   this._stateLeaf = stateLeaf;
  1216   this._uploadInProgress = false;
  1218   AbstractHealthReporter.call(this, branch, policy, sessionRecorder);
  1220   if (!this.serverURI) {
  1221     throw new Error("No server URI defined. Did you forget to define the pref?");
  1224   if (!this.serverNamespace) {
  1225     throw new Error("No server namespace defined. Did you forget a pref?");
  1228   this._state = new HealthReporterState(this);
  1231 this.HealthReporter.prototype = Object.freeze({
  1232   __proto__: AbstractHealthReporter.prototype,
  1234   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
  1236   get lastSubmitID() {
  1237     return this._state.lastSubmitID;
  1238   },
  1240   /**
  1241    * When we last successfully submitted data to the server.
  1243    * This is sent as part of the upload. This is redundant with similar data
  1244    * in the policy because we like the modules to be loosely coupled and the
  1245    * similar data in the policy is only used for forensic purposes.
  1246    */
  1247   get lastPingDate() {
  1248     return this._state.lastPingDate;
  1249   },
  1251   /**
  1252    * The base URI of the document server to which to submit data.
  1254    * This is typically a Bagheera server instance. It is the URI up to but not
  1255    * including the version prefix. e.g. https://data.metrics.mozilla.com/
  1256    */
  1257   get serverURI() {
  1258     return this._prefs.get("documentServerURI", null);
  1259   },
  1261   set serverURI(value) {
  1262     if (!value) {
  1263       throw new Error("serverURI must have a value.");
  1266     if (typeof(value) != "string") {
  1267       throw new Error("serverURI must be a string: " + value);
  1270     this._prefs.set("documentServerURI", value);
  1271   },
  1273   /**
  1274    * The namespace on the document server to which we will be submitting data.
  1275    */
  1276   get serverNamespace() {
  1277     return this._prefs.get("documentServerNamespace", "metrics");
  1278   },
  1280   set serverNamespace(value) {
  1281     if (!value) {
  1282       throw new Error("serverNamespace must have a value.");
  1285     if (typeof(value) != "string") {
  1286       throw new Error("serverNamespace must be a string: " + value);
  1289     this._prefs.set("documentServerNamespace", value);
  1290   },
  1292   /**
  1293    * Whether this instance will upload data to a server.
  1294    */
  1295   get willUploadData() {
  1296     return this._policy.dataSubmissionPolicyAccepted &&
  1297            this._policy.healthReportUploadEnabled;
  1298   },
  1300   /**
  1301    * Whether remote data is currently stored.
  1303    * @return bool
  1304    */
  1305   haveRemoteData: function () {
  1306     return !!this._state.lastSubmitID;
  1307   },
  1309   /**
  1310    * Called to initiate a data upload.
  1312    * The passed argument is a `DataSubmissionRequest` from policy.jsm.
  1313    */
  1314   requestDataUpload: function (request) {
  1315     if (!this._initialized) {
  1316       return Promise.reject(new Error("Not initialized."));
  1319     return Task.spawn(function doUpload() {
  1320       yield this._providerManager.ensurePullOnlyProvidersRegistered();
  1321       try {
  1322         yield this.collectMeasurements();
  1323         try {
  1324           yield this._uploadData(request);
  1325         } catch (ex) {
  1326           this._onSubmitDataRequestFailure(ex);
  1328       } finally {
  1329         yield this._providerManager.ensurePullOnlyProvidersUnregistered();
  1331     }.bind(this));
  1332   },
  1334   /**
  1335    * Request that server data be deleted.
  1337    * If deletion is scheduled to occur immediately, a promise will be returned
  1338    * that will be fulfilled when the deletion attempt finishes. Otherwise,
  1339    * callers should poll haveRemoteData() to determine when remote data is
  1340    * deleted.
  1341    */
  1342   requestDeleteRemoteData: function (reason) {
  1343     if (!this.haveRemoteData()) {
  1344       return;
  1347     return this._policy.deleteRemoteData(reason);
  1348   },
  1350   _initializeState: function() {
  1351     return this._state.init();
  1352   },
  1354   /**
  1355    * Override default handler to incur an upload describing the error.
  1356    */
  1357   _onInitError: function (error) {
  1358     // Need to capture this before we call the parent else it's always
  1359     // set.
  1360     let inShutdown = this._shutdownRequested;
  1362     let result;
  1363     try {
  1364       result = AbstractHealthReporter.prototype._onInitError.call(this, error);
  1365     } catch (ex) {
  1366       this._log.error("Error when calling _onInitError: " +
  1367                       CommonUtils.exceptionStr(ex));
  1370     // This bypasses a lot of the checks in policy, such as respect for
  1371     // backoff. We should arguably not do this. However, reporting
  1372     // startup errors is important. And, they should not occur with much
  1373     // frequency in the wild. So, it shouldn't be too big of a deal.
  1374     if (!inShutdown &&
  1375         this._policy.ensureNotifyResponse(new Date()) &&
  1376         this._policy.healthReportUploadEnabled) {
  1377       // We don't care about what happens to this request. It's best
  1378       // effort.
  1379       let request = {
  1380         onNoDataAvailable: function () {},
  1381         onSubmissionSuccess: function () {},
  1382         onSubmissionFailureSoft: function () {},
  1383         onSubmissionFailureHard: function () {},
  1384         onUploadInProgress: function () {},
  1385       };
  1387       this._uploadData(request);
  1390     return result;
  1391   },
  1393   _onBagheeraResult: function (request, isDelete, date, result) {
  1394     this._log.debug("Received Bagheera result.");
  1396     return Task.spawn(function onBagheeraResult() {
  1397       let hrProvider = this.getProvider("org.mozilla.healthreport");
  1399       if (!result.transportSuccess) {
  1400         // The built-in provider may not be initialized if this instance failed
  1401         // to initialize fully.
  1402         if (hrProvider && !isDelete) {
  1403           hrProvider.recordEvent("uploadTransportFailure", date);
  1406         request.onSubmissionFailureSoft("Network transport error.");
  1407         throw new Task.Result(false);
  1410       if (!result.serverSuccess) {
  1411         if (hrProvider && !isDelete) {
  1412           hrProvider.recordEvent("uploadServerFailure", date);
  1415         request.onSubmissionFailureHard("Server failure.");
  1416         throw new Task.Result(false);
  1419       if (hrProvider && !isDelete) {
  1420         hrProvider.recordEvent("uploadSuccess", date);
  1423       if (isDelete) {
  1424         this._log.warn("Marking delete as successful.");
  1425         yield this._state.removeRemoteIDs([result.id]);
  1426       } else {
  1427         this._log.warn("Marking upload as successful.");
  1428         yield this._state.updateLastPingAndRemoveRemoteIDs(date, result.deleteIDs);
  1431       request.onSubmissionSuccess(this._now());
  1433       throw new Task.Result(true);
  1434     }.bind(this));
  1435   },
  1437   _onSubmitDataRequestFailure: function (error) {
  1438     this._log.error("Error processing request to submit data: " +
  1439                     CommonUtils.exceptionStr(error));
  1440   },
  1442   _formatDate: function (date) {
  1443     // Why, oh, why doesn't JS have a strftime() equivalent?
  1444     return date.toISOString().substr(0, 10);
  1445   },
  1447   _uploadData: function (request) {
  1448     // Under ideal circumstances, clients should never race to this
  1449     // function. However, server logs have observed behavior where
  1450     // racing to this function could be a cause. So, this lock was
  1451     // instituted.
  1452     if (this._uploadInProgress) {
  1453       this._log.warn("Upload requested but upload already in progress.");
  1454       let provider = this.getProvider("org.mozilla.healthreport");
  1455       let promise = provider.recordEvent("uploadAlreadyInProgress");
  1456       request.onUploadInProgress("Upload already in progress.");
  1457       return promise;
  1460     let id = CommonUtils.generateUUID();
  1462     this._log.info("Uploading data to server: " + this.serverURI + " " +
  1463                    this.serverNamespace + ":" + id);
  1464     let client = new BagheeraClient(this.serverURI);
  1465     let now = this._now();
  1467     return Task.spawn(function doUpload() {
  1468       try {
  1469         // The test for upload locking monkeypatches getJSONPayload.
  1470         // If the next two lines change, be sure to verify the test is
  1471         // accurate!
  1472         this._uploadInProgress = true;
  1473         let payload = yield this.getJSONPayload();
  1475         let histogram = Services.telemetry.getHistogramById(TELEMETRY_PAYLOAD_SIZE_UNCOMPRESSED);
  1476         histogram.add(payload.length);
  1478         let lastID = this.lastSubmitID;
  1479         yield this._state.addRemoteID(id);
  1481         let hrProvider = this.getProvider("org.mozilla.healthreport");
  1482         if (hrProvider) {
  1483           let event = lastID ? "continuationUploadAttempt"
  1484                              : "firstDocumentUploadAttempt";
  1485           hrProvider.recordEvent(event, now);
  1488         TelemetryStopwatch.start(TELEMETRY_UPLOAD, this);
  1489         let result;
  1490         try {
  1491           let options = {
  1492             deleteIDs: this._state.remoteIDs.filter((x) => { return x != id; }),
  1493             telemetryCompressed: TELEMETRY_PAYLOAD_SIZE_COMPRESSED,
  1494           };
  1495           result = yield client.uploadJSON(this.serverNamespace, id, payload,
  1496                                            options);
  1497           TelemetryStopwatch.finish(TELEMETRY_UPLOAD, this);
  1498         } catch (ex) {
  1499           TelemetryStopwatch.cancel(TELEMETRY_UPLOAD, this);
  1500           if (hrProvider) {
  1501             hrProvider.recordEvent("uploadClientFailure", now);
  1503           throw ex;
  1506         yield this._onBagheeraResult(request, false, now, result);
  1507       } finally {
  1508         this._uploadInProgress = false;
  1510     }.bind(this));
  1511   },
  1513   /**
  1514    * Request deletion of remote data.
  1516    * @param request
  1517    *        (DataSubmissionRequest) Tracks progress of this request.
  1518    */
  1519   deleteRemoteData: function (request) {
  1520     if (!this._state.lastSubmitID) {
  1521       this._log.info("Received request to delete remote data but no data stored.");
  1522       request.onNoDataAvailable();
  1523       return;
  1526     this._log.warn("Deleting remote data.");
  1527     let client = new BagheeraClient(this.serverURI);
  1529     return Task.spawn(function* doDelete() {
  1530       try {
  1531         let result = yield client.deleteDocument(this.serverNamespace,
  1532                                                  this.lastSubmitID);
  1533         yield this._onBagheeraResult(request, true, this._now(), result);
  1534       } catch (ex) {
  1535         this._log.error("Error processing request to delete data: " +
  1536                         CommonUtils.exceptionStr(error));
  1537       } finally {
  1538         // If we don't have any remote documents left, nuke the ID.
  1539         // This is done for privacy reasons. Why preserve the ID if we
  1540         // don't need to?
  1541         if (!this.haveRemoteData()) {
  1542           yield this._state.resetClientID();
  1545     }.bind(this));
  1546   },
  1547 });

mercurial