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
--- 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