Bug 761045: Upload locally installed apps on first run; r=gps
authorAnant Narayanan <anant@kix.in>
Fri, 13 Jul 2012 19:52:30 -0700
changeset 99305 1808d668c711d6b2fa7b4fe5ece0646e930478a3
parent 99304 1498357e31445a39a8191dea0224c126c5ef7cc9
child 99306 66a9982a4bc9ca3284a1bc0cf139a0eee386bac6
push id23117
push usergszorc@mozilla.com
push dateSat, 14 Jul 2012 20:35:19 +0000
treeherdermozilla-central@8e2f9cc15bd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs761045
milestone16.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 761045: Upload locally installed apps on first run; r=gps
dom/apps/src/Webapps.jsm
services/aitc/modules/browserid.js
services/aitc/modules/client.js
services/aitc/modules/main.js
services/aitc/modules/manager.js
services/aitc/tests/unit/test_aitc_manager.js
services/aitc/tests/unit/xpcshell.ini
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -604,17 +604,17 @@ let DOMApplicationRegistry = {
   /** Added to support AITC and classic sync */
   itemExists: function(aId) {
     return !!this.webapps[aId];
   },
 
   getAppById: function(aId) {
     if (!this.webapps[aId])
       return null;
-    
+
     let app = this._cloneAppObject(this.webapps[aId]);
     return app;
   },
 
   getAppByManifestURL: function(aManifestURL) {
     // This could be O(1) if |webapps| was a dictionary indexed on manifestURL
     // which should be the unique app identifier.
     // It's currently O(n).
--- a/services/aitc/modules/browserid.js
+++ b/services/aitc/modules/browserid.js
@@ -133,16 +133,21 @@ BrowserIDService.prototype = {
   /**
    * Internal implementation methods begin here
    */
 
   // Try to get the user's email(s). If user isn't logged in, this will be empty
   _getEmails: function _getEmails(cb, options, sandbox) {
     let self = this;
 
+    if (!sandbox) {
+      cb(new Error("Sandbox not created"), null);
+      return;
+    }
+
     function callback(res) {
       let emails = {};
       try {
         emails = JSON.parse(res);
       } catch (e) {
         self._log.error("Exception in JSON.parse for _getAssertion: " + e);
       }
       self._gotEmails(emails, sandbox, cb, options);
@@ -377,18 +382,28 @@ BrowserIDService.prototype = {
  *
  * @param cb
  *        (function) Callback to be invoked with a Sandbox, when ready.
  * @param uri
  *        (String) URI to be loaded in the Sandbox.
  */
 function Sandbox(cb, uri) {
   this._uri = uri;
-  this._createFrame();
-  this._createSandbox(cb, uri);
+
+  // Put in a try/catch block because Services.wm.getMostRecentWindow, called in
+  // _createFrame will be null in XPCShell.
+  try {
+    this._createFrame();
+    this._createSandbox(cb, uri);
+  } catch(e) {
+    this._log = Log4Moz.repository.getLogger("Service.AITC.BrowserID.Sandbox");
+    this._log.level = Log4Moz.Level[PREFS.get("log")];
+    this._log.error("Could not create Sandbox " + e);
+    cb(null);
+  }
 }
 Sandbox.prototype = {
   /**
    * Frees the sandbox and releases the iframe created to host it.
    */
   free: function free() {
     delete this.box;
     this._container.removeChild(this._frame);
--- a/services/aitc/modules/client.js
+++ b/services/aitc/modules/client.js
@@ -186,17 +186,17 @@ AitcClient.prototype = {
       cb(new Error("Exception in getApps " + e), null);
       return;
     }
 
     // Return success.
     try {
       cb(null, apps);
       // Don't update lastModified until we know cb succeeded.
-      this._appsLastModified = parseInt(req.response.headers["X-Timestamp"], 10);
+      this._appsLastModified = parseInt(req.response.headers["x-timestamp"], 10);
       this._state.set("lastModified", ""  + this._appsLastModified);
     } catch (e) {
       this._log.error("Exception in getApps callback " + e);
     }
   },
 
   /**
    * Change a given app record to match what the server expects.
@@ -371,17 +371,17 @@ AitcClient.prototype = {
     this._backoff = false;
     this._state.set("backoff", "0");
     return true;
   },
 
   // Set values from X-Backoff and Retry-After headers, if present.
   _setBackoff: function _setBackoff(req) {
     let backoff = 0;
-    let successfulStatusCodes = [200, 201, 204, 304, 401];
+    let statusCodesWithoutBackoff = [200, 201, 204, 304, 401];
 
     let val;
     if (req.response.headers["Retry-After"]) {
       val = req.response.headers["Retry-After"];
       backoff = parseInt(val, 10);
       this._log.warn("Retry-Header header was seen: " + val);
     } else if (req.response.headers["X-Backoff"]) {
       val = req.response.headers["X-Backoff"];
--- a/services/aitc/modules/main.js
+++ b/services/aitc/modules/main.js
@@ -23,24 +23,37 @@ function Aitc() {
     "services.aitc.service.log.level"
   )];
   this._log.info("Loading AitC");
 
   this.DASHBOARD_ORIGIN = CommonUtils.makeURI(
     Preferences.get("services.aitc.dashboard.url")
   ).prePath;
 
-  this._manager = new AitcManager(this._init.bind(this));
+  let self = this;
+  this._manager = new AitcManager(function managerDone() {
+    CommonUtils.nextTick(self._init, self);
+  });
 }
 Aitc.prototype = {
   // The goal of the init function is to be ready to activate the AITC
-  // client whenever the user is looking at the dashboard.
-  _init: function init() {
+  // client whenever the user is looking at the dashboard. It also calls
+  // the initialSchedule function on the manager.
+  _init: function _init() {
     let self = this;
 
+    // Do an initial upload.
+    this._manager.initialSchedule(function queueDone(num) {
+      if (num == -1) {
+        self._log.debug("No initial upload was required");
+        return;
+      }
+      self._log.debug(num + " initial apps queued successfully");
+    });
+
     // This is called iff the user is currently looking the dashboard.
     function dashboardLoaded(browser) {
       let win = browser.contentWindow;
       self._log.info("Dashboard was accessed " + win);
 
       // If page is ready to go, fire immediately.
       if (win.document && win.document.readyState == "complete") {
         self._manager.userActive(win);
--- a/services/aitc/modules/manager.js
+++ b/services/aitc/modules/manager.js
@@ -37,17 +37,17 @@ function AitcManager(cb, premadeClient, 
   this._client = null;
   this._getTimer = null;
   this._putTimer = null;
 
   this._lastTokenTime = 0;
   this._tokenDuration = INITIAL_TOKEN_DURATION;
   this._premadeToken = premadeToken || null;
   this._invalidTokenFlag = false;
-  
+
   this._lastEmail = null;
   this._dashboardWindow = null;
 
   this._log = Log4Moz.repository.getLogger("Service.AITC.Manager");
   this._log.level = Log4Moz.Level[Preferences.get("manager.log.level")];
   this._log.info("Loading AitC manager module");
 
   // Check if we have pending PUTs from last time.
@@ -62,23 +62,19 @@ function AitcManager(cb, premadeClient, 
     }
 
     // Used for testing.
     if (premadeClient) {
       self._client = premadeClient;
       cb(null, true);
       return;
     }
-    // Schedule them, but only if we can get a silent assertion.
-    self._makeClient(function(err, client) {
-      if (!err && client) {
-        self._client = client;
-        self._processQueue();
-      }
-    }, false);
+
+    // Caller will invoke initialSchedule which will process any items in the
+    // queue, if present.
   });
 }
 AitcManager.prototype = {
   /**
    * State of the user. ACTIVE implies user is looking at the dashboard,
    * PASSIVE means either not at the dashboard or the idle timer started.
    */
   _ACTIVE: 1,
@@ -165,16 +161,81 @@ AitcManager.prototype = {
    * page, a call to userActive MUST be made.
    */
   userIdle: function userIdle() {
     this._state = this._PASSIVE;
     this._dashboardWindow = null;
   },
 
   /**
+   * Initial schedule for the manager. It is the responsibility of the
+   * caller who created this object to call this function if it wants to
+   * do an initial sync (i.e. upload local apps on a device that has never
+   * communicated with AITC before).
+   *
+   * The callback will be invoked with the number of local apps that were
+   * queued to be uploaded, or -1 if this client has already synced and a
+   * local upload is not required.
+   *
+   * Try to schedule PUTs but only if we can get a silent assertion, and if
+   * the queue in non-empty, or we've never done a GET (first run).
+   */
+  initialSchedule: function initialSchedule(cb) {
+    let self = this;
+
+    function startProcessQueue(num) {
+      self._makeClient(function(err, client) {
+        if (!err && client) {
+          self._client = client;
+          self._processQueue();
+          return;
+        }
+      });
+      cb(num);
+    }
+
+    // If we've already done a sync with AITC, it means we've already done
+    // an initial upload. Resume processing the queue, if there are items in it.
+    if (Preferences.get("services.aitc.client.lastModified", "0") != "0") {
+      if (this._pending.length) {
+        startProcessQueue(-1);
+      } else {
+        cb(-1);
+      }
+      return;
+    }
+
+    DOMApplicationRegistry.getAllWithoutManifests(function gotAllApps(apps) {
+      let done = 0;
+      let appids = Object.keys(apps);
+      let total = appids.length;
+      self._log.info("First run, queuing all local apps: " + total + " found");
+
+      function appQueued(err) {
+        if (err) {
+          self._log.error("Error queuing app " + apps[appids[done]].origin);
+        }
+
+        if (done == total) {
+          self._log.info("Finished queuing all initial local apps");
+          startProcessQueue(total);
+          return;
+        }
+
+        let app = apps[appids[done]];
+        let obj = {type: "install", app: app, retries: 0, lastTime: 0};
+
+        done += 1;
+        self._pending.enqueue(obj, appQueued);
+      }
+      appQueued();
+    });
+  },
+
+  /**
    * Poll the AITC server for any changes and process them. It is safe to call
    * this function multiple times. Last caller wins. The function will
    * grab the current user state from _state and act accordingly.
    *
    * Invalid states will cause this function to throw.
    */
   _setPoll: function _setPoll() {
     if (this._state == this._ACTIVE && !this._client) {
@@ -192,17 +253,17 @@ AitcManager.prototype = {
 
     // Check if there are any PUTs pending first.
     if (this._pending.length && !(this._putTimer)) {
       // There are pending PUTs and no timer, so let's process them. GETs will
       // resume after the PUTs finish (see processQueue)
       this._processQueue();
       return;
     }
-    
+
     // Do one GET soon, but only if user is active.
     let getFreq;
     if (this._state == this._ACTIVE) {
       CommonUtils.nextTick(this._checkServer, this);
       getFreq = PREFS.get("manager.getActiveFreq");
     } else {
       getFreq = PREFS.get("manager.getPassiveFreq");
     }
@@ -476,24 +537,24 @@ AitcManager.prototype = {
         return;
       }
 
       // Silent refresh was asked for.
       if (!win) {
         cb(err, null);
         return;
       }
-      
+
       // Prompt user to login.
       self._makeClient(function(err, client) {
         if (err) {
           cb(err, null);
           return;
         }
-      
+
         // makeClient sets an updated token.
         self._client = client;
         self._invalidTokenFlag = false;
         cb(null, true);
       }, win);
     }
 
     let options = { audience: DASHBOARD_URL };
@@ -536,17 +597,17 @@ AitcManager.prototype = {
   _gotToken: function _gotToken(err, tok, cb) {
     if (!err) {
       this._log.info("Got token from server: " + JSON.stringify(tok));
       this._tokenDuration = parseInt(tok.duration, 10);
       cb(null, tok);
       return;
     }
 
-    let msg = err.name + " in _getToken: " + err.error;
+    let msg = "Error in _getToken: " + err;
     this._log.error(msg);
     cb(msg, null);
   },
 
   // Extract the email address from a BrowserID assertion.
   _extractEmail: function _extractEmail(assertion) {
     // Please look the other way while I do this. Thanks.
     let chain = assertion.split("~");
--- a/services/aitc/tests/unit/test_aitc_manager.js
+++ b/services/aitc/tests/unit/test_aitc_manager.js
@@ -1,17 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+Cu.import("resource://gre/modules/Webapps.jsm");
+
+Cu.import("resource://services-aitc/client.js");
+Cu.import("resource://services-aitc/manager.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/preferences.js");
-Cu.import("resource://services-aitc/client.js");
-Cu.import("resource://services-aitc/manager.js");
+
 Cu.import("resource://testing-common/services-common/aitcserver.js");
 
 const PREFS = new Preferences("services.aitc.");
 
 let count = 0;
 
 function run_test() {
   initTestLogging("Trace");
@@ -72,41 +75,104 @@ function get_client_for_server(username,
   return new AitcClient(token, new Preferences("services.aitc.client."));
 }
 
 // Check that a is less than b.
 function do_check_lt(a, b) {
   do_check_true(a < b);
 }
 
+add_test(function test_manager_localapps() {
+  // Install two fake apps into the DOM registry.
+  let fakeApp1 = get_mock_app();
+  fakeApp1.manifest = {
+    name: "Appasaurus 1",
+    description: "One of the best fake apps ever",
+    launch_path: "/",
+    fullscreen: true,
+    required_features: ["webgl"]
+  };
+
+  let fakeApp2 = get_mock_app();
+  fakeApp2.manifest = {
+    name: "Appasaurus 2",
+    description: "The other best fake app ever",
+    launch_path: "/",
+    fullscreen: true,
+    required_features: ["geolocation"]
+  };
+
+  DOMApplicationRegistry.confirmInstall({app: fakeApp1});
+  DOMApplicationRegistry.confirmInstall({app: fakeApp2});
+
+  // Create an instance of the manager and check if it put the app in the queue.
+  // We put doInitialUpload in nextTick, because maanger will not be defined
+  // in the callback. This pattern is used everywhere, AitcManager is created.
+  let manager = new AitcManager(function() {
+    CommonUtils.nextTick(doInitialUpload);
+  });
+
+  function doInitialUpload() {
+    manager.initialSchedule(function(num) {
+      // 2 apps should have been queued.
+      do_check_eq(num, 2);
+      do_check_eq(manager._pending.length, 2);
+
+      let entry = manager._pending.peek();
+      do_check_eq(entry.type, "install");
+      do_check_eq(entry.app.origin, fakeApp1.origin);
+
+      // Remove one app from queue.
+      manager._pending.dequeue(run_next_test);
+    });
+  }
+});
+
+add_test(function test_manager_alreadysynced() {
+  // The manager should ignore any local apps if we've already synced before.
+  Preferences.set("services.aitc.client.lastModified", "" + Date.now());
+
+  let manager = new AitcManager(function() {
+    CommonUtils.nextTick(doCheck);
+  });
+
+  function doCheck() {
+    manager.initialSchedule(function(num) {
+      do_check_eq(num, -1);
+      do_check_eq(manager._pending.length, 1);
+      // Clear queue for next test.
+      manager._pending.dequeue(run_next_test);
+    });
+  }
+});
+
 add_test(function test_401_responses() {
   PREFS.set("client.backoff", "50");
   PREFS.set("manager.putFreq", 50);
   const app = get_mock_app();
   const username = "123";
   const premadeToken = {
     id: "testtest",
     key: "testtest",
     endpoint: "http://localhost:8080/1.0/123",
     uid: "uid",
     duration: "5000"
   };
+
   let server = get_server_with_user(username);
+  let client = get_client_for_server(username, server);
+
   server.mockStatus = {
     code: 401,
     method: "Unauthorized"
-  }
-  let client = get_client_for_server(username, server);
-  let manager = new AitcManager(function () {}, client, premadeToken);
-  // Assume first token is not out dated.
-  manager._lastTokenTime = Date.now();
+  };
+
   let mockRequestCount = 0;
   let clientFirstToken = null;
-
-  server.onRequest = function mockstatus () {
+  server.onRequest = function mockstatus() {
     mockRequestCount++;
     switch (mockRequestCount) {
       case 1:
         clientFirstToken = client.token;
         // Switch to using mock 201s.
         this.mockStatus = {
           code: 201,
           method: "Created"
@@ -116,17 +182,25 @@ add_test(function test_401_responses() {
         // Check that the client obtained a different token.
         do_check_neq(client.token.id, clientFirstToken.id);
         do_check_neq(client.token.key, clientFirstToken.key);
         server.stop(run_next_test);
         break;
     }
   }
 
-  manager.appEvent("install", get_mock_app());
+  let manager = new AitcManager(function() {
+    CommonUtils.nextTick(gotManager);
+  }, client, premadeToken);
+
+  function gotManager() {
+    // Assume first token is not outdated.
+    manager._lastTokenTime = Date.now();
+    manager.appEvent("install", get_mock_app());
+  }
 });
 
 add_test(function test_client_exponential_backoff() {
   _("Test that the client is properly setting the backoff");
 
   // Use prefs to speed up tests.
   const putFreq = 50;
   const initialBackoff = 50;
--- a/services/aitc/tests/unit/xpcshell.ini
+++ b/services/aitc/tests/unit/xpcshell.ini
@@ -1,9 +1,9 @@
 [DEFAULT]
 head = ../../../common/tests/unit/head_global.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
 tail =
 
 [test_load_modules.js]
+[test_aitc_client.js]
+[test_aitc_manager.js]
 [test_storage_queue.js]
-[test_storage_registry.js]
-[test_aitc_client.js]
-[test_aitc_manager.js]
\ No newline at end of file
+[test_storage_registry.js]
\ No newline at end of file