Bug 1255040 - Implement mozAddonManager.createInstall(). r=rhelmer

MozReview-Commit-ID: JLrhGywROzt

--HG--
extra : transplant_source : h%01%A8%D1%89%C0IO%1E%879C%01%25%ECW%1Dg%1D%7C
This commit is contained in:
Andrew Swan 2016-04-18 13:48:12 -07:00
Родитель 5f6c846326
Коммит dc4b339b75
9 изменённых файлов: 552 добавлений и 40 удалений

Просмотреть файл

@ -2784,13 +2784,110 @@ var AddonManagerInternal = {
},
webAPI: {
getAddonByID(id) {
// installs maps integer ids to AddonInstall instances.
installs: new Map(),
nextInstall: 0,
sendEvent: null,
setEventHandler(fn) {
this.sendEvent = fn;
},
getAddonByID(target, id) {
return new Promise(resolve => {
AddonManager.getAddonByID(id, (addon) => {
resolve(webAPIForAddon(addon));
});
});
}
},
// helper to copy (and convert) the properties we care about
copyProps(install, obj) {
obj.state = AddonManager.stateToString(install.state);
obj.error = AddonManager.errorToString(install.error);
obj.progress = install.progress;
obj.maxProgress = install.maxProgress;
},
makeListener(id, target) {
const events = [
"onDownloadStarted",
"onDownloadProgress",
"onDownloadEnded",
"onDownloadCancelled",
"onDownloadFailed",
"onInstallStarted",
"onInstallEnded",
"onInstallCancelled",
"onInstallFailed",
];
let listener = {};
events.forEach(event => {
listener[event] = (install) => {
let data = {event, id};
AddonManager.webAPI.copyProps(install, data);
this.sendEvent(target, data);
}
});
return listener;
},
forgetInstall(id) {
let info = this.installs.get(id);
if (!info) {
throw new Error(`forgetInstall cannot find ${id}`);
}
info.install.removeListener(info.listener);
this.installs.delete(id);
},
createInstall(target, options) {
return new Promise((resolve) => {
let newInstall = install => {
let id = this.nextInstall++;
let listener = this.makeListener(id, target);
install.addListener(listener);
this.installs.set(id, {install, target, listener});
let result = {id};
this.copyProps(install, result);
resolve(result);
};
AddonManager.getInstallForURL(options.url, newInstall, "application/x-xpinstall");
});
},
addonInstallDoInstall(target, id) {
let state = this.installs.get(id);
if (!state) {
return Promise.reject(`invalid id ${id}`);
}
return Promise.resolve(state.install.install());
},
addonInstallCancel(target, id) {
let state = this.installs.get(id);
if (!state) {
return Promise.reject(`invalid id ${id}`);
}
return Promise.resolve(state.install.cancel());
},
clearInstalls(ids) {
for (let id of ids) {
this.forgetInstall(id);
}
},
clearInstallsFrom(mm) {
for (let [id, info] of this.installs) {
if (info.target == mm) {
this.forgetInstall(id);
}
}
},
},
};
@ -2951,39 +3048,45 @@ this.AddonManagerPrivate = {
*/
this.AddonManager = {
// Constants for the AddonInstall.state property
// The install is available for download.
STATE_AVAILABLE: 0,
// The install is being downloaded.
STATE_DOWNLOADING: 1,
// The install is checking for compatibility information.
STATE_CHECKING: 2,
// The install is downloaded and ready to install.
STATE_DOWNLOADED: 3,
// The download failed.
STATE_DOWNLOAD_FAILED: 4,
// The add-on is being installed.
STATE_INSTALLING: 5,
// The add-on has been installed.
STATE_INSTALLED: 6,
// The install failed.
STATE_INSTALL_FAILED: 7,
// The install has been cancelled.
STATE_CANCELLED: 8,
// These will show up as AddonManager.STATE_* (eg, STATE_AVAILABLE)
_states: new Map([
// The install is available for download.
["STATE_AVAILABLE", 0],
// The install is being downloaded.
["STATE_DOWNLOADING", 1],
// The install is checking for compatibility information.
["STATE_CHECKING", 2],
// The install is downloaded and ready to install.
["STATE_DOWNLOADED", 3],
// The download failed.
["STATE_DOWNLOAD_FAILED", 4],
// The add-on is being installed.
["STATE_INSTALLING", 5],
// The add-on has been installed.
["STATE_INSTALLED", 6],
// The install failed.
["STATE_INSTALL_FAILED", 7],
// The install has been cancelled.
["STATE_CANCELLED", 8],
]),
// Constants representing different types of errors while downloading an
// add-on.
// The download failed due to network problems.
ERROR_NETWORK_FAILURE: -1,
// The downloaded file did not match the provided hash.
ERROR_INCORRECT_HASH: -2,
// The downloaded file seems to be corrupted in some way.
ERROR_CORRUPT_FILE: -3,
// An error occured trying to write to the filesystem.
ERROR_FILE_ACCESS: -4,
// The add-on must be signed and isn't.
ERROR_SIGNEDSTATE_REQUIRED: -5,
// The downloaded add-on had a different type than expected.
ERROR_UNEXPECTED_ADDON_TYPE: -6,
// These will show up as AddonManager.ERROR_* (eg, ERROR_NETWORK_FAILURE)
_errors: new Map([
// The download failed due to network problems.
["ERROR_NETWORK_FAILURE", -1],
// The downloaded file did not match the provided hash.
["ERROR_INCORRECT_HASH", -2],
// The downloaded file seems to be corrupted in some way.
["ERROR_CORRUPT_FILE", -3],
// An error occured trying to write to the filesystem.
["ERROR_FILE_ACCESS", -4],
// The add-on must be signed and isn't.
["ERROR_SIGNEDSTATE_REQUIRED", -5],
// The downloaded add-on had a different type than expected.
["ERROR_UNEXPECTED_ADDON_TYPE", -6],
]),
// These must be kept in sync with AddonUpdateChecker.
// No error was encountered.
@ -3165,6 +3268,27 @@ this.AddonManager = {
return gStartupComplete && !gShutdownInProgress;
},
init() {
this._stateToString = new Map();
for (let [name, value] of this._states) {
this[name] = value;
this._stateToString.set(value, name);
}
this._errorToString = new Map();
for (let [name, value] of this._errors) {
this[name] = value;
this._errorToString.set(value, name);
}
},
stateToString(state) {
return this._stateToString.get(state);
},
errorToString(err) {
return err ? this._errorToString.get(err) : null;
},
getInstallForURL: function(aUrl, aCallback, aMimetype,
aHash, aName, aIcons,
aVersion, aBrowser) {
@ -3382,6 +3506,8 @@ this.AddonManager = {
},
};
this.AddonManager.init();
// load the timestamps module into AddonManagerInternal
Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", AddonManagerInternal);
Object.freeze(AddonManagerInternal);

Просмотреть файл

@ -26,6 +26,8 @@ const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
const MSG_INSTALL_EVENT = "WebAPIInstallEvent";
const MSG_INSTALL_CLEANUP = "WebAPICleanup";
const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
@ -48,6 +50,12 @@ function amManager() {
gParentMM = Services.ppmm;
gParentMM.addMessageListener(MSG_INSTALL_ENABLED, this);
gParentMM.addMessageListener(MSG_PROMISE_REQUEST, this);
gParentMM.addMessageListener(MSG_INSTALL_CLEANUP, this);
Services.obs.addObserver(this, "message-manager-close", false);
Services.obs.addObserver(this, "message-manager-disconnect", false);
AddonManager.webAPI.setEventHandler(this.sendEvent);
// Needed so receiveMessage can be called directly by JS callers
this.wrappedJSObject = this;
@ -55,8 +63,16 @@ function amManager() {
amManager.prototype = {
observe: function(aSubject, aTopic, aData) {
if (aTopic == "addons-startup")
AddonManagerPrivate.startup();
switch (aTopic) {
case "addons-startup":
AddonManagerPrivate.startup();
break;
case "message-manager-close":
case "message-manager-disconnect":
AddonManager.webAPI.clearInstallsFrom(aSubject);
break;
}
},
/**
@ -193,17 +209,26 @@ amManager.prototype = {
let API = AddonManager.webAPI;
if (payload.type in API) {
API[payload.type](...payload.args).then(resolve, reject);
API[payload.type](aMessage.target, ...payload.args).then(resolve, reject);
}
else {
reject("Unknown Add-on API request.");
}
break;
}
case MSG_INSTALL_CLEANUP: {
AddonManager.webAPI.clearInstalls(payload.ids);
break;
}
}
return undefined;
},
sendEvent(target, data) {
target.sendAsyncMessage(MSG_INSTALL_EVENT, data);
},
classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"),
_xpcom_factory: {
createInstance: function(aOuter, aIid) {

Просмотреть файл

@ -12,6 +12,8 @@ Cu.import("resource://gre/modules/Task.jsm");
const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
const MSG_INSTALL_EVENT = "WebAPIInstallEvent";
const MSG_INSTALL_CLEANUP = "WebAPICleanup";
const APIBroker = {
_nextID: 0,
@ -19,7 +21,11 @@ const APIBroker = {
init() {
this._promises = new Map();
// _installMap maps integer ids to DOM AddonInstall instances
this._installMap = new Map();
Services.cpmm.addMessageListener(MSG_PROMISE_RESULT, this);
Services.cpmm.addMessageListener(MSG_INSTALL_EVENT, this);
},
receiveMessage(message) {
@ -40,6 +46,17 @@ const APIBroker = {
reject(payload.reject);
break;
}
case MSG_INSTALL_EVENT: {
let install = this._installMap.get(payload.id);
if (!install) {
let err = new Error(`Got install event for unknown install ${payload.id}`);
Cu.reportError(err);
return;
}
install._dispatch(payload);
break;
}
}
},
@ -51,6 +68,10 @@ const APIBroker = {
Services.cpmm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
});
},
sendCleanup: function(ids) {
Services.cpmm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids });
},
};
APIBroker.init();
@ -67,6 +88,18 @@ function Addon(win, properties) {
};
}
function AddonInstall(window, properties) {
let id = properties.id;
APIBroker._installMap.set(id, this);
this.window = window;
this.handlers = new Map();
for (let key of Object.keys(properties)) {
this[key] = properties[key];
}
}
/**
* API methods should return promises from the page, this is a simple wrapper
* to make sure of that. It also automatically wraps objects when necessary.
@ -81,6 +114,9 @@ function WebAPITask(generator) {
if (obj instanceof Addon) {
return win.Addon._create(win, obj);
}
if (obj instanceof AddonInstall) {
return win.AddonInstall._create(win, obj);
}
return obj;
}
@ -92,12 +128,50 @@ function WebAPITask(generator) {
}
}
const INSTALL_EVENTS = [
"onDownloadStarted",
"onDownloadProgress",
"onDownloadEnded",
"onDownloadCancelled",
"onDownloadFailed",
"onInstallStarted",
"onInstallEnded",
"onInstallCancelled",
"onInstallFailed",
];
AddonInstall.prototype = {
_dispatch(data) {
// The message for the event includes updated copies of all install
// properties. Use the usual "let webidl filter visible properties" trick.
for (let key of Object.keys(data)) {
this[key] = data[key];
}
let event = new this.window.Event(data.event);
this.__DOM_IMPL__.dispatchEvent(event);
},
install: WebAPITask(function*() {
yield APIBroker.sendRequest("addonInstallDoInstall", this.id);
}),
cancel: WebAPITask(function*() {
yield APIBroker.sendRequest("addonInstallCancel", this.id);
}),
};
function WebAPI() {
}
WebAPI.prototype = {
init(window) {
this.window = window;
this.allInstalls = [];
window.addEventListener("unload", event => {
APIBroker.sendCleanup(this.allInstalls);
});
},
getAddonByID: WebAPITask(function*(id) {
@ -105,10 +179,15 @@ WebAPI.prototype = {
return addonInfo ? new Addon(this.window, addonInfo) : null;
}),
createInstall() {
let err = new this.window.Error("not yet implemented");
return this.window.Promise.reject(err);
},
createInstall: WebAPITask(function*(options) {
let installInfo = yield APIBroker.sendRequest("createInstall", options);
if (!installInfo) {
return null;
}
let install = new AddonInstall(this.window, installInfo);
this.allInstalls.push(installInfo.id);
return install;
}),
classID: Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"),
contractID: "@mozilla.org/addon-web-api/manager;1",

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,9 @@
Components.utils.import("resource://gre/modules/Services.jsm");
function startup(data, reason) {
Services.prefs.setIntPref("webapitest.active_version", 1);
}
function shutdown(data, reason) {
Services.prefs.setIntPref("webapitest.active_version", 0);
}

Просмотреть файл

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>webapi_install@tests.mozilla.org</em:id>
<em:version>1.1</em:version>
<em:name>AddonManger web API test</em:name>
<em:bootstrap>true</em:bootstrap>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>0.3</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:targetApplication>
<Description>
<em:id>toolkit@mozilla.org</em:id>
<em:minVersion>0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

Просмотреть файл

@ -66,5 +66,6 @@ skip-if = require_signing
[browser_update.js]
[browser_webapi.js]
[browser_webapi_access.js]
[browser_webapi_install.js]
[include:browser-common.ini]

Просмотреть файл

@ -0,0 +1,242 @@
const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
const XPI_URL = `${SECURE_TESTROOT}addons/browser_webapi_install.xpi`;
const ID = "webapi_install@tests.mozilla.org";
// eh, would be good to just stat the real file instead of this...
const XPI_LEN = 4782;
Services.prefs.setBoolPref("extensions.webapi.testing", true);
Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("extensions.webapi.testing");
Services.prefs.clearUserPref("extensions.install.requireBuiltInCerts");
});
function waitForClear() {
const MSG = "WebAPICleanup";
return new Promise(resolve => {
let listener = {
receiveMessage: function(msg) {
if (msg.name == MSG) {
Services.ppmm.removeMessageListener(MSG, listener);
resolve();
}
}
};
Services.ppmm.addMessageListener(MSG, listener);
});
}
// Wrapper around a common task to run in the content process to test
// the mozAddonManager API. Takes a URL for the XPI to install and an
// array of steps, each of which can either be an action to take
// (i.e., start or cancel the install) or an install event to wait for.
// Steps that look for a specific event may also include a "props" property
// with properties that the AddonInstall object is expected to have when
// that event is triggered.
function* testInstall(browser, url, steps, description) {
let success = yield ContentTask.spawn(browser, {url, steps}, function* (opts) {
let { url, steps } = opts;
let install = yield content.navigator.mozAddonManager.createInstall({url});
if (!install) {
yield Promise.reject("createInstall() did not return an install object");
}
// Check that the initial state of the AddonInstall is sane.
if (install.state != "STATE_AVAILABLE") {
yield Promise.reject("new install should be in STATE_AVAILABLE");
}
if (install.error != null) {
yield Promise.reject("new install should have null error");
}
const events = [
"onDownloadStarted",
"onDownloadProgress",
"onDownloadEnded",
"onDownloadCancelled",
"onDownloadFailed",
"onInstallStarted",
"onInstallEnded",
"onInstallCancelled",
"onInstallFailed",
];
let eventWaiter = null;
let receivedEvents = [];
events.forEach(event => {
install.addEventListener(event, e => {
receivedEvents.push({
event,
state: install.state,
error: install.error,
progress: install.progress,
maxProgress: install.maxProgress,
});
if (eventWaiter) {
eventWaiter();
}
});
});
// Returns a promise that is resolved when the given event occurs
// or rejects if a different event comes first or if props is supplied
// and properties on the AddonInstall don't match those in props.
function expectEvent(event, props) {
return new Promise((resolve, reject) => {
function check() {
let received = receivedEvents.shift();
if (received.event != event) {
let err = new Error(`expected ${event} but got ${received.event}`);
reject(err);
}
if (props) {
for (let key of Object.keys(props)) {
if (received[key] != props[key]) {
throw new Error(`AddonInstall property ${key} was ${received[key]} but expected ${props[key]}`);
}
}
}
resolve();
}
if (receivedEvents.length > 0) {
check();
} else {
eventWaiter = () => {
eventWaiter = null;
check();
}
}
});
}
while (steps.length > 0) {
let nextStep = steps.shift();
if (nextStep.action) {
if (nextStep.action == "install") {
yield install.install();
} else if (nextStep.action == "cancel") {
yield install.cancel();
} else {
throw new Error(`unknown action ${nextStep.action}`);
}
} else {
yield expectEvent(nextStep.event, nextStep.props);
}
}
return true;
});
is(success, true, description);
}
function makeInstallTest(task) {
return function*() {
// withNewTab() will close the test tab before returning, at which point
// the cleanup event will come from the content process. We need to see
// that event but don't want to race to install a listener for it after
// the tab is closed. So set up the listener now but don't yield the
// listening promise until below.
let clearPromise = waitForClear();
yield BrowserTestUtils.withNewTab(TESTPAGE, task);
yield clearPromise;
is(AddonManager.webAPI.installs.size, 0, "AddonInstall was cleaned up");
};
}
// Check the happy path for installing an add-on using the mozAddonManager API.
add_task(makeInstallTest(function* (browser) {
let steps = [
{action: "install"},
{
event: "onDownloadStarted",
props: {state: "STATE_DOWNLOADING"},
},
{
event: "onDownloadProgress",
props: {maxProgress: XPI_LEN},
},
{
event: "onDownloadEnded",
props: {
state: "STATE_DOWNLOADED",
progress: XPI_LEN,
maxProgress: XPI_LEN,
},
},
{
event: "onInstallStarted",
props: {state: "STATE_INSTALLING"},
},
{
event: "onInstallEnded",
props: {state: "STATE_INSTALLED"},
},
];
yield testInstall(browser, XPI_URL, steps, "a basic install works");
let version = Services.prefs.getIntPref("webapitest.active_version");
is(version, 1, "the install really did work");
// Sanity check to ensure that the test in makeInstallTest() that
// installs.size == 0 means we actually did clean up.
ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
let addons = yield promiseAddonsByIDs([ID]);
isnot(addons[0], null, "Found the addon");
yield addons[0].uninstall();
addons = yield promiseAddonsByIDs([ID]);
is(addons[0], null, "Addon was uninstalled");
}));
add_task(makeInstallTest(function* (browser) {
let steps = [
{action: "cancel"},
{
event: "onDownloadCancelled",
props: {
state: "STATE_CANCELLED",
error: null,
},
}
];
yield testInstall(browser, XPI_URL, steps, "canceling an install works");
let addons = yield promiseAddonsByIDs([ID]);
is(addons[0], null, "The addon was not installed");
ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
}));
add_task(makeInstallTest(function* (browser) {
let steps = [
{action: "install"},
{
event: "onDownloadStarted",
props: {state: "STATE_DOWNLOADING"},
},
{event: "onDownloadProgress"},
{
event: "onDownloadFailed",
props: {
state: "STATE_DOWNLOAD_FAILED",
error: "ERROR_NETWORK_FAILURE",
},
}
];
yield testInstall(browser, XPI_URL + "bogus", steps, "a basic install works");
let addons = yield promiseAddonsByIDs([ID]);
is(addons[0], null, "The addon was not installed");
ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
}));

Просмотреть файл

@ -10,7 +10,8 @@ const IGNORE = ["getPreferredIconURL", "escapeAddonURI",
"addAddonListener", "removeAddonListener",
"addInstallListener", "removeInstallListener",
"addManagerListener", "removeManagerListener",
"mapURIToAddonID", "shutdown"];
"mapURIToAddonID", "shutdown", "init",
"stateToString", "errorToString"];
const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
"AddonScreenshot", "AddonType", "startup", "shutdown",