Bug 811159 - Save last submitted health report to profile; r=rnewman
authorGregory Szorc <gps@mozilla.com>
Fri, 16 Nov 2012 10:05:19 -0800
changeset 113831 e76044ee76e32fc04d29741639cc3d2cabd5f9e5
parent 113830 213ad3540ebc91dffbde6b9b47753fea9bd19a08
child 113832 e1a3bae737a7528ebedc23fa1c224c5954add62d
push id23890
push userryanvm@gmail.com
push dateWed, 21 Nov 2012 02:43:32 +0000
treeherdermozilla-central@4f19e7fd8bea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs811159
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 811159 - Save last submitted health report to profile; r=rnewman
services/healthreport/healthreporter.jsm
services/healthreport/tests/xpcshell/head.js
services/healthreport/tests/xpcshell/test_healthreporter.js
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -9,16 +9,17 @@ this.EXPORTED_SYMBOLS = ["HealthReporter
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-common/bagheeraclient.js");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
 Cu.import("resource://gre/modules/services/metrics/collector.jsm");
 
 
 // Oldest year to allow in date preferences. This module was implemented in
 // 2012 and no dates older than that should be encountered.
 const OLDEST_ALLOWED_YEAR = 2012;
 
@@ -145,23 +146,34 @@ HealthReporter.prototype = {
    *
    * @return bool
    */
   haveRemoteData: function haveRemoteData() {
     return !!this.lastSubmitID;
   },
 
   /**
-   * Start background functionality.
+   * Perform post-construction initialization and start background activity.
    *
    * If this isn't called, no data upload will occur.
+   *
+   * This returns a promise that will be fulfilled when all initialization
+   * activity is completed. It is not safe for this instance to perform
+   * additional actions until this promise has been resolved.
    */
   start: function start() {
-    this._policy.startPolling();
-    this._log.info("HealthReporter started.");
+    let onExists = function onExists() {
+      this._policy.startPolling();
+      this._log.info("HealthReporter started.");
+
+      return Promise.resolve();
+    }.bind(this);
+
+    return this._ensureDirectoryExists(this._stateDir)
+               .then(onExists);
   },
 
   /**
    * Stop background functionality.
    */
   stop: function stop() {
     this._policy.stopPolling();
   },
@@ -365,22 +377,23 @@ HealthReporter.prototype = {
     let id = CommonUtils.generateUUID();
 
     this._log.info("Uploading data to server: " + this.serverURI + " " +
                    this.serverNamespace + ":" + id);
     let client = new BagheeraClient(this.serverURI);
 
     let payload = this.getJSONPayload();
 
-    let promise = client.uploadJSON(this.serverNamespace,
-                                    id,
-                                    payload,
-                                    this.lastSubmitID);
-
-    return promise.then(this._onBagheeraResult.bind(this, request, false));
+    return this._saveLastPayload(payload)
+               .then(client.uploadJSON.bind(client,
+                                            this.serverNamespace,
+                                            id,
+                                            payload,
+                                            this.lastSubmitID))
+               .then(this._onBagheeraResult.bind(this, request, false));
   },
 
   _deleteRemoteData: function _deleteRemoteData(request) {
     if (!this.lastSubmitID) {
       this._log.info("Received request to delete remote data but no data stored.");
       request.onNoDataAvailable();
       return;
     }
@@ -389,16 +402,87 @@ HealthReporter.prototype = {
     let client = new BagheeraClient(this.serverURI);
 
     return client.deleteDocument(this.serverNamespace, this.lastSubmitID)
                  .then(this._onBagheeraResult.bind(this, request, true),
                        this._onSubmitDataRequestFailure.bind(this));
 
   },
 
+  get _stateDir() {
+    let profD = OS.Constants.Path.profileDir;
+
+    // Work around bug 810543 until OS.File is more resilient.
+    if (!profD || !profD.length) {
+      throw new Error("Could not obtain profile directory. OS.File not " +
+                      "initialized properly?");
+    }
+
+    return OS.Path.join(profD, "healthreport");
+  },
+
+  _ensureDirectoryExists: function _ensureDirectoryExists(path) {
+    let deferred = Promise.defer();
+
+    OS.File.makeDir(path).then(
+      function onResult() {
+        deferred.resolve(true);
+      },
+      function onError(error) {
+        if (error.becauseExists) {
+          deferred.resolve(true);
+          return;
+        }
+
+        deferred.reject(error);
+      }
+    );
+
+    return deferred.promise;
+  },
+
+  get _lastPayloadPath() {
+    return OS.Path.join(this._stateDir, "lastpayload.json");
+  },
+
+  _saveLastPayload: function _saveLastPayload(payload) {
+    let path = this._lastPayloadPath;
+    let pathTmp = path + ".tmp";
+
+    let encoder = new TextEncoder();
+    let buffer = encoder.encode(payload);
+
+    return OS.File.writeAtomic(path, buffer, {tmpPath: pathTmp});
+  },
+
+  /**
+   * Obtain the last uploaded payload.
+   *
+   * The promise is resolved to a JSON-decoded object on success. The promise
+   * is rejected if the last uploaded payload could not be found or there was
+   * an error reading or parsing it.
+   *
+   * @return Promise<object>
+   */
+  getLastPayload: function getLoadPayload() {
+    let path = this._lastPayloadPath;
+
+    return OS.File.read(path).then(
+      function onData(buffer) {
+        let decoder = new TextDecoder();
+        let json = JSON.parse(decoder.decode(buffer));
+
+        return Promise.resolve(json);
+      },
+      function onError(error) {
+        return Promise.reject(error);
+      }
+    );
+  },
+
   _now: function _now() {
     return new Date();
   },
 
   //-----------------------------
   // HealthReportPolicy listeners
   //-----------------------------
 
--- a/services/healthreport/tests/xpcshell/head.js
+++ b/services/healthreport/tests/xpcshell/head.js
@@ -1,13 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+// We need to initialize the profile or OS.File may not work. See bug 810543.
+do_get_profile();
+
 (function initMetricsTestingInfrastructure() {
   let ns = {};
   Components.utils.import("resource://testing-common/services-common/logging.js",
                           ns);
 
   ns.initTestLogging();
 }).call(this);
 
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -91,16 +91,24 @@ add_test(function test_register_provider
   let reporter = getReporter("category_manager");
   do_check_eq(reporter._collector._providers.length, 0);
   reporter.registerProvidersFromCategoryManager(category);
   do_check_eq(reporter._collector._providers.length, 1);
 
   run_next_test();
 });
 
+add_test(function test_start() {
+  let reporter = getReporter("start");
+  reporter.start().then(function onStarted() {
+    reporter.stop();
+    run_next_test();
+  });
+});
+
 add_test(function test_json_payload_simple() {
   let reporter = getReporter("json_payload_simple");
 
   let now = new Date();
   let payload = reporter.getJSONPayload();
   let original = JSON.parse(payload);
 
   do_check_eq(original.version, 1);
@@ -255,8 +263,23 @@ add_test(function test_policy_accept_rej
 
   reporter.recordPolicyRejection();
   do_check_false(reporter.dataSubmissionPolicyAccepted);
   do_check_false(reporter.willUploadData);
 
   server.stop(run_next_test);
 });
 
+
+add_test(function test_upload_save_payload() {
+  let [reporter, server] = getReporterAndServer("upload_save_payload");
+
+  let deferred = Promise.defer();
+  let request = new DataSubmissionRequest(deferred, new Date(), false);
+
+  reporter._uploadData(request).then(function onUpload() {
+    reporter.getLastPayload().then(function onJSON(json) {
+      do_check_true("thisPingDate" in json);
+      server.stop(run_next_test);
+    });
+  });
+});
+