Bug 1298025 - Move addon installation internally to Marionette; r?automatedtester r?ahal draft
authorAndreas Tolfsen <ato@mozilla.com>
Fri, 26 Aug 2016 13:38:03 +0100
changeset 439274 da649becc922644e1c3c9467424fe9dd08e07eae
parent 439149 f8ba9c9b401f57b0047ddd6932cb830190865b38
child 537119 15a3516c863f784013b504fd5ec3c68855e23e31
push id35951
push userbmo:ato@mozilla.com
push dateTue, 15 Nov 2016 18:33:28 +0000
reviewersautomatedtester, ahal
bugs1298025
milestone53.0a1
Bug 1298025 - Move addon installation internally to Marionette; r?automatedtester r?ahal Addons can be installed and uninstalled using the Marionette client utility found in testing/marionette/client/marionette_driver/addons.py. It injects system-privileged chrome JavaScript to manipulate the addon manager service. To make this feature more widely adopted, i.e. by other clients such as WebDriver, this patch moves the addon installation code to internally in Marionette and exposes them as so called WebDriver extension commands. This patch also _explicitly breaks_ backwards compatibility with older Geckos that do not support the new Marionette:installAddon and Marionette:uninstallAddon commands. MozReview-Commit-ID: 18IceiGIg5H
testing/marionette/addon.js
testing/marionette/client/marionette_driver/addons.py
testing/marionette/driver.js
testing/marionette/jar.mn
--- a/testing/marionette/addon.js
+++ b/testing/marionette/addon.js
@@ -1,93 +1,104 @@
 /* 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, results: Cr} = Components;
+const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 
 Cu.import("chrome://marionette/content/error.js");
 
 this.EXPORTED_SYMBOLS = ["addon"];
 
 this.addon = {};
 
+// from https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager/AddonManager#AddonInstall_errors
+addon.Errors = {
+  [-1]: "ERROR_NETWORK_FAILURE: A network error occured.",
+  [-2]: "ERROR_INCORECT_HASH: The downloaded file did not match the expected hash.",
+  [-3]: "ERROR_CORRUPT_FILE: The file appears to be corrupt.",
+  [-4]: "ERROR_FILE_ACCESS: There was an error accessing the filesystem.",
+  [-5]: "ERROR_SIGNEDSTATE_REQUIRED: The addon must be signed and isn't.",
+};
+
+function lookupError(code) {
+  let msg = addon.Errors[code];
+  return new UnknownError(msg);
+}
+
 /**
- * Installs Firefox addon.
+ * Install a Firefox addon.
  *
- * If the addon is restartless, it can be used right away. Otherwise a
- * restart is needed.
+ * If the addon is restartless, it can be used right away.  Otherwise a
+ * restart is required.
  *
- * Temporary addons will automatically be unisntalled on shutdown and
+ * Temporary addons will automatically be uninstalled on shutdown and
  * do not need to be signed, though they must be restartless.
  *
  * @param {string} path
- *     Full path to the extension package archive to be installed.
+ *     Full path to the extension package archive.
  * @param {boolean=} temporary
- *     Install the add-on temporarily if true.
+ *     True to install the addon temporarily, false (default) otherwise.
  *
- * @return {Promise.<string>}
- *     Addon ID string of the newly installed addon.
+ * @return {Promise: string}
+ *     Addon ID.
  *
- * @throws {AddonError}
- *     if installation fails
+ * @throws {UnknownError}
+ *     If there is a problem installing the addon.
  */
 addon.install = function(path, temporary = false) {
   return new Promise((resolve, reject) => {
+    let file = new FileUtils.File(path);
+
     let listener = {
       onInstallEnded: function(install, addon) {
         resolve(addon.id);
       },
 
       onInstallFailed: function(install) {
-        reject(new AddonError(install.error));
+        reject(lookupError(install.error));
       },
 
       onInstalled: function(addon) {
         AddonManager.removeAddonListener(listener);
         resolve(addon.id);
       }
     };
 
-    let file = new FileUtils.File(path);
-
-    // temporary addons
-    if (temp) {
-      AddonManager.addAddonListener(listener);
-      AddonManager.installTemporaryAddon(file);
-    }
-
-    // addons that require restart
-    else {
+    if (!temporary) {
       AddonManager.getInstallForFile(file, function(aInstall) {
-        if (aInstall.error != 0) {
-          reject(new AddonError(aInstall.error));
+        if (aInstall.error !== 0) {
+          reject(lookupError(aInstall.error));
         }
         aInstall.addListener(listener);
         aInstall.install();
       });
+    } else {
+      AddonManager.addAddonListener(listener);
+      AddonManager.installTemporaryAddon(file);
     }
   });
 };
 
 /**
  * Uninstall a Firefox addon.
  *
- * If the addon is restartless, it will be uninstalled right
- * away. Otherwise a restart is necessary.
+ * If the addon is restartless it will be uninstalled right away.
+ * Otherwise, Firefox must be restarted for the change to take effect.
  *
  * @param {string} id
- *     Addon ID to uninstall.
+ *     ID of the addon to uninstall.
  *
  * @return {Promise}
  */
 addon.uninstall = function(id) {
   return new Promise(resolve => {
-    AddonManager.getAddonByID(arguments[0], function(addon) {
+    AddonManager.getAddonByID(id, function(addon) {
       addon.uninstall();
+      resolve();
     });
   });
 };
--- a/testing/marionette/client/marionette_driver/addons.py
+++ b/testing/marionette/client/marionette_driver/addons.py
@@ -1,120 +1,70 @@
 # 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/.
 
-from .errors import MarionetteException
+from . import errors
 
-__all__ = ['Addons', 'AddonInstallException']
+__all__ = ["Addons", "AddonInstallException"]
 
 
-# From https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager/AddonManager#AddonInstall_errors
-ADDON_INSTALL_ERRORS = {
-    -1: "ERROR_NETWORK_FAILURE: A network error occured.",
-    -2: "ERROR_INCORECT_HASH: The downloaded file did not match the expected hash.",
-    -3: "ERROR_CORRUPT_FILE: The file appears to be corrupt.",
-    -4: "ERROR_FILE_ACCESS: There was an error accessing the filesystem.",
-    -5: "ERROR_SIGNEDSTATE_REQUIRED: The addon must be signed and isn't.",
-}
-
-
-class AddonInstallException(MarionetteException):
+class AddonInstallException(errors.MarionetteException):
     pass
 
 
 class Addons(object):
-    """
-    An API for installing and inspecting addons during Gecko runtime. This
-    is a partially implemented wrapper around Gecko's `AddonManager API`_.
+    """An API for installing and inspecting addons during Gecko
+    runtime. This is a partially implemented wrapper around Gecko's
+    `AddonManager API`_.
 
     For example::
 
         from marionette_driver.addons import Addons
         addons = Addons(marionette)
-        addons.install('path/to/extension.xpi')
+        addons.install("/path/to/extension.xpi")
 
     .. _AddonManager API: https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager
+
     """
-
     def __init__(self, marionette):
         self._mn = marionette
 
     def install(self, path, temp=False):
-        """Install an addon.
+        """Install a Firefox addon.
 
         If the addon is restartless, it can be used right away. Otherwise
         a restart using :func:`~marionette_driver.marionette.Marionette.restart`
         will be needed.
 
         :param path: A file path to the extension to be installed.
         :param temp: Install a temporary addon. Temporary addons will
                      automatically be uninstalled on shutdown and do not need
                      to be signed, though they must be restartless.
+
         :returns: The addon ID string of the newly installed addon.
+
         :raises: :exc:`AddonInstallException`
-        """
-        with self._mn.using_context('chrome'):
-            addon_id, status = self._mn.execute_async_script("""
-              let fileUtils = Components.utils.import("resource://gre/modules/FileUtils.jsm");
-              let FileUtils = fileUtils.FileUtils;
-              Components.utils.import("resource://gre/modules/AddonManager.jsm");
-              let listener = {
-                onInstallEnded: function(install, addon) {
-                  marionetteScriptFinished([addon.id, 0]);
-                },
-
-                onInstallFailed: function(install) {
-                  marionetteScriptFinished([null, install.error]);
-                },
-
-                onInstalled: function(addon) {
-                  AddonManager.removeAddonListener(listener);
-                  marionetteScriptFinished([addon.id, 0]);
-                }
-              }
 
-              let file = new FileUtils.File(arguments[0]);
-              let temp = arguments[1];
-
-              if (!temp) {
-                AddonManager.getInstallForFile(file, function(aInstall) {
-                  if (aInstall.error != 0) {
-                    marionetteScriptFinished([null, aInstall.error]);
-                  }
-                  aInstall.addListener(listener);
-                  aInstall.install();
-                });
-              } else {
-                AddonManager.addAddonListener(listener);
-                AddonManager.installTemporaryAddon(file);
-              }
-            """, script_args=[path, temp], debug_script=True)
-
-        if status:
-            if status in ADDON_INSTALL_ERRORS:
-                raise AddonInstallException(ADDON_INSTALL_ERRORS[status])
-            raise AddonInstallException(
-                "Addon failed to install with return code: {}".format(status))
-        return addon_id
+        """
+        body = {"path": path, "temporary": temp}
+        try:
+            return self._mn._send_message("addon:install", body, key="value")
+        except errors.UnknownException as e:
+            raise AddonInstallException(e)
 
     def uninstall(self, addon_id):
-        """Uninstall an addon.
+        """Uninstall a Firefox addon.
 
         If the addon is restartless, it will be uninstalled right away.
         Otherwise a restart using :func:`~marionette_driver.marionette.Marionette.restart`
         will be needed.
 
         If the call to uninstall is resulting in a `ScriptTimeoutException`,
         an invalid ID is likely being passed in. Unfortunately due to
         AddonManager's implementation, it's hard to retrieve this error from
         Python.
 
         :param addon_id: The addon ID string to uninstall.
+
         """
-        with self._mn.using_context('chrome'):
-            return self._mn.execute_async_script("""
-              Components.utils.import("resource://gre/modules/AddonManager.jsm");
-              AddonManager.getAddonByID(arguments[0], function(addon) {
-                addon.uninstall();
-                marionetteScriptFinished(0);
-              });
-            """, script_args=[addon_id])
+        body = {"id": addon_id}
+        self._mn._send_message("addon:uninstall", body)
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -13,16 +13,18 @@ Cu.import("resource://gre/modules/Log.js
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
 
 Cu.import("chrome://marionette/content/accessibility.js");
+Cu.import("chrome://marionette/content/action.js");
+Cu.import("chrome://marionette/content/addon.js");
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/browser.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
@@ -2600,16 +2602,44 @@ GeckoDriver.prototype.quitApplication = 
 
   this._server.acceptConnections = false;
   resp.send();
 
   this.sessionTearDown();
   Services.startup.quit(flags);
 };
 
+GeckoDriver.prototype.installAddon = function(cmd, resp) {
+  if (this.appName != "Firefox") {
+    throw new UnsupportedOperationError();
+  }
+
+  let path = cmd.parameters.path;
+  let temp = cmd.parameters.temporary || false;
+  if (typeof path == "undefined" || typeof path != "string" ||
+      typeof temp != "boolean") {
+    throw InvalidArgumentError();
+  }
+
+  return addon.install(path, temp);
+};
+
+GeckoDriver.prototype.uninstallAddon = function(cmd, resp) {
+  if (this.appName != "Firefox") {
+    throw new UnsupportedOperationError();
+  }
+
+  let id = cmd.parameters.id;
+  if (typeof id == "undefined" || typeof id != "string") {
+    throw new InvalidArgumentError();
+  }
+
+  return addon.uninstall(id);
+};
+
 /**
  * Helper function to convert an outerWindowID into a UID that Marionette
  * tracks.
  */
 GeckoDriver.prototype.generateFrameId = function(id) {
   let uid = id + (this.appName == "B2G" ? "-b2g" : "");
   return uid;
 };
@@ -2855,9 +2885,12 @@ GeckoDriver.prototype.commands = {
   "acceptDialog": GeckoDriver.prototype.acceptDialog,
   "getTextFromDialog": GeckoDriver.prototype.getTextFromDialog,
   "sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog,
   "acceptConnections": GeckoDriver.prototype.acceptConnections,
   "quitApplication": GeckoDriver.prototype.quitApplication,
 
   "localization:l10n:localizeEntity": GeckoDriver.prototype.localizeEntity,
   "localization:l10n:localizeProperty": GeckoDriver.prototype.localizeProperty,
+
+  "addon:install": GeckoDriver.prototype.installAddon,
+  "addon:uninstall": GeckoDriver.prototype.uninstallAddon,
 };
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -24,16 +24,17 @@ marionette.jar:
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)
   content/atom.js (atom.js)
   content/evaluate.js (evaluate.js)
   content/logging.js (logging.js)
   content/navigate.js (navigate.js)
   content/l10n.js (l10n.js)
   content/assert.js (assert.js)
+  content/addon.js (addon.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (harness/marionette/chrome/test.xul)
   content/test2.xul  (harness/marionette/chrome/test2.xul)
   content/test_dialog.xul  (harness/marionette/chrome/test_dialog.xul)
   content/test_nested_iframe.xul  (harness/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (harness/marionette/chrome/test_anonymous_content.xul)
 #endif