Bug 1359558 Part 2 Pass appropriate bootstrap reasons when temporarily installing an addon on top of an existing one r=kmag

Prior to this patch we always passed APP_SHUTDOWN as the reason when
shutting down and uninstall a temporary addon, even if the same addon
was installed permanently.  Now we send an appropriate reason.

Also untangled a bunch of stuff that had been added to the test case
for temporary-addon-installed-over-permanently-installed-addon and
then extended the basic test to cover this scenario.

MozReview-Commit-ID: 7rgfpiRYcFu

--HG--
extra : rebase_source : e916e040000838b22d6f30ac226115e953f2a3a9
This commit is contained in:
Andrew Swan 2017-07-31 15:21:57 -07:00
parent d1026f07aa
commit b8e80aa938
2 changed files with 249 additions and 195 deletions

View File

@ -450,6 +450,21 @@ function findMatchingStaticBlocklistItem(aAddon) {
return null;
}
/**
* Determine the reason to pass to an extension's bootstrap methods when
* switch between versions.
*
* @param {string} oldVersion The version of the existing extension instance.
* @param {string} newVersion The version of the extension being installed.
*
* @return {BOOSTRAP_REASONS.ADDON_UPGRADE|BOOSTRAP_REASONS.ADDON_DOWNGRADE}
*/
function newVersionReason(oldVersion, newVersion) {
return Services.vc.compare(oldVersion, newVersion) <= 0 ?
BOOTSTRAP_REASONS.ADDON_UPGRADE :
BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
}
/**
* Converts an iterable of addon objects into a map with the add-on's ID as key.
*/
@ -1646,12 +1661,13 @@ this.XPIStates = {
* Find the highest priority location of an add-on by ID and return the
* location and the XPIState.
* @param aId The add-on ID
* @param aLocations If specified, the locations to search
* @return {XPIState?}
*/
findAddon(aId) {
findAddon(aId, aLocations = this.db.values()) {
// Fortunately the Map iterator returns in order of insertion, which is
// also our highest -> lowest priority order.
for (let location of this.db.values()) {
for (let location of aLocations) {
if (location.has(aId)) {
return location.get(aId);
}
@ -2221,13 +2237,20 @@ this.XPIProvider = {
// If the add-on was pending disable then shut it down and remove it
// from the persisted data.
let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
if (addon.disable) {
XPIProvider.callBootstrapMethod(addonDetails, addon.file, "shutdown",
BOOTSTRAP_REASONS.ADDON_DISABLE);
} else {
XPIProvider.callBootstrapMethod(addonDetails, addon.file, "shutdown",
BOOTSTRAP_REASONS.APP_SHUTDOWN);
reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
} else if (addon.location.name == KEY_APP_TEMPORARY) {
reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
let locations = Array.from(XPIStates.db.values())
.filter(loc => loc.name != TemporaryInstallLocation.name);
let existing = XPIStates.findAddon(addon.id, locations);
if (existing) {
reason = newVersionReason(addon.version, existing.version);
}
}
XPIProvider.callBootstrapMethod(addonDetails, addon.file,
"shutdown", reason);
}
Services.obs.removeObserver(this, "quit-application-granted");
}
@ -2313,21 +2336,28 @@ this.XPIProvider = {
for (let [id, addon] of tempLocation.entries()) {
tempLocation.delete(id);
let reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
let locations = Array.from(XPIStates.db.values())
.filter(loc => loc != tempLocation);
let existing = XPIStates.findAddon(id, locations);
if (existing) {
reason = newVersionReason(addon.version, existing.version);
}
this.callBootstrapMethod(createAddonDetails(id, addon),
addon.file, "uninstall",
BOOTSTRAP_REASONS.ADDON_UNINSTALL);
addon.file, "uninstall", reason);
this.unloadBootstrapScope(id);
TemporaryInstallLocation.uninstallAddon(id);
let state = XPIStates.findAddon(id);
if (state) {
let newAddon = XPIDatabase.makeAddonLocationVisible(id, state.location.name);
if (existing) {
let newAddon = XPIDatabase.makeAddonLocationVisible(id, existing.location.name);
let file = new nsIFile(newAddon.path);
let data = {oldVersion: addon.version};
this.callBootstrapMethod(createAddonDetails(id, newAddon),
file, "install",
BOOTSTRAP_REASONS.ADDON_INSTALL);
file, "install", reason, data);
}
}
}
@ -2822,9 +2852,7 @@ this.XPIProvider = {
// call its uninstall method
let newVersion = addon.version;
let oldVersion = existingAddon;
let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
BOOTSTRAP_REASONS.ADDON_UPGRADE :
BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
let uninstallReason = newVersionReason(oldVersion, newVersion);
this.callBootstrapMethod(createAddonDetails(existingAddonID, existingAddon),
file, "uninstall", uninstallReason,
@ -3445,11 +3473,7 @@ this.XPIProvider = {
let newVersion = addon.version;
let oldVersion = oldAddon.version;
if (Services.vc.compare(newVersion, oldVersion) >= 0) {
installReason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
} else {
installReason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
}
installReason = newVersionReason(oldVersion, newVersion);
let uninstallReason = installReason;
extraParams.newVersion = newVersion;

View File

@ -2,6 +2,8 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
Components.utils.import("resource://gre/modules/AppConstants.jsm");
const ID = "bootstrap1@tests.mozilla.org";
const sampleRDFManifest = {
id: ID,
@ -22,7 +24,9 @@ BootstrapMonitor.init();
// Partial list of bootstrap reasons from XPIProvider.jsm
const BOOTSTRAP_REASONS = {
APP_STARTUP: 1,
ADDON_INSTALL: 5,
ADDON_UNINSTALL: 6,
ADDON_UPGRADE: 7,
ADDON_DOWNGRADE: 8,
};
@ -100,8 +104,17 @@ add_task(async function() {
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
let onShutdown = waitForBootstrapEvent("shutdown", ID);
let onUninstall = waitForBootstrapEvent("uninstall", ID);
await promiseRestartManager();
let shutdown = await onShutdown;
equal(shutdown.reason, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
let uninstall = await onUninstall;
equal(uninstall.reason, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(ID);
@ -114,15 +127,15 @@ add_task(async function() {
// Install a temporary add-on over the top of an existing add-on.
// Restart and make sure the existing add-on comes back.
add_task(async function() {
await promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
await promiseInstallAllFiles([do_get_addon("test_bootstrap1_2")], true);
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
BootstrapMonitor.checkAddonInstalled(ID, "2.0");
BootstrapMonitor.checkAddonStarted(ID, "2.0");
let addon = await promiseAddonByID(ID);
do_check_neq(addon, null);
do_check_eq(addon.version, "1.0");
do_check_eq(addon.version, "2.0");
do_check_eq(addon.name, "Test Bootstrap 1");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
@ -132,187 +145,135 @@ add_task(async function() {
let tempdir = gTmpD.clone();
// test that an unpacked add-on works too
writeInstallRDFToDir({
id: ID,
version: "3.0",
bootstrap: true,
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test Bootstrap 1 (temporary)",
}, tempdir, "bootstrap1@tests.mozilla.org", "bootstrap.js");
let bootstrapJS = await OS.File.read("data/test_temporary/bootstrap.js", {encoding: "utf-8"});
let unpacked_addon = tempdir.clone();
unpacked_addon.append(ID);
do_get_file("data/test_temporary/bootstrap.js")
.copyTo(unpacked_addon, "bootstrap.js");
await AddonManager.installTemporaryAddon(unpacked_addon);
BootstrapMonitor.checkAddonInstalled(ID, "3.0");
BootstrapMonitor.checkAddonStarted(ID, "3.0");
addon = await promiseAddonByID(ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "3.0");
do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
restartManager();
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
addon = await promiseAddonByID(ID);
// existing add-on is back
do_check_neq(addon, null);
do_check_eq(addon.version, "1.0");
do_check_eq(addon.name, "Test Bootstrap 1");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
unpacked_addon.remove(true);
// on Windows XPI files will be locked by the JAR cache, skip this test there.
if (!("nsIWindowsRegKey" in Components.interfaces)) {
// test that a packed (XPI) add-on works
writeInstallRDFToXPI({
id: ID,
version: "2.0",
bootstrap: true,
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test Bootstrap 1 (temporary)",
}, tempdir, "bootstrap1@tests.mozilla.org");
let packed_addon = tempdir.clone();
packed_addon.append(ID + ".xpi");
await AddonManager.installTemporaryAddon(packed_addon);
addon = await promiseAddonByID(ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "2.0");
do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
restartManager();
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
addon = await promiseAddonByID(ID);
// existing add-on is back
do_check_neq(addon, null);
do_check_eq(addon.version, "1.0");
do_check_eq(addon.name, "Test Bootstrap 1");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
packed_addon.remove(false);
// test that a webextension works
let webext = createTempWebExtensionFile({
manifest: {
version: "4.0",
name: "Test WebExtension 1 (temporary)",
applications: {
gecko: {
id: ID
}
}
for (let newversion of ["1.0", "3.0"]) {
for (let packed of [false, true]) {
// ugh, file locking issues with xpis on windows
if (packed && AppConstants.platform == "win") {
continue;
}
});
await Promise.all([
AddonManager.installTemporaryAddon(webext),
promiseWebExtensionStartup(),
]);
addon = await promiseAddonByID(ID);
let files = {
"install.rdf": AddonTestUtils.createInstallRDF({
id: ID,
version: newversion,
bootstrap: true,
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test Bootstrap 1 (temporary)",
}),
"bootstrap.js": bootstrapJS,
};
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "4.0");
do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
let target;
if (packed) {
target = tempdir.clone();
target.append(ID);
// test that re-loading a webextension works, using the same filename
webext.remove(false);
webext = createTempWebExtensionFile({
manifest: {
version: "5.0",
name: "Test WebExtension 1 (temporary)",
applications: {
gecko: {
id: ID
}
}
await AddonTestUtils.promiseWriteFilesToDir(target.path, files);
} else {
target = tempdir.clone();
target.append(`${ID}.xpi`);
await AddonTestUtils.promiseWriteFilesToZip(target.path, files);
}
});
await Promise.all([
AddonManager.installTemporaryAddon(webext),
promiseWebExtensionStartup(),
]);
addon = await promiseAddonByID(ID);
let onShutdown = waitForBootstrapEvent("shutdown", ID);
let onUninstall = waitForBootstrapEvent("uninstall", ID);
let onInstall = waitForBootstrapEvent("install", ID);
let onStartup = waitForBootstrapEvent("startup", ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "5.0");
do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_true(addon.isWebExtension);
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
await AddonManager.installTemporaryAddon(target);
restartManager();
let reason = Services.vc.compare(newversion, "2.0") < 0 ?
BOOTSTRAP_REASONS.ADDON_DOWNGRADE :
BOOTSTRAP_REASONS.ADDON_UPGRADE;
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
let shutdown = await onShutdown;
equal(shutdown.data.version, "2.0");
equal(shutdown.reason, reason);
addon = await promiseAddonByID(ID);
let uninstall = await onUninstall;
equal(uninstall.data.version, "2.0");
equal(uninstall.reason, reason);
// existing add-on is back
do_check_neq(addon, null);
do_check_eq(addon.version, "1.0");
do_check_eq(addon.name, "Test Bootstrap 1");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
let install = await onInstall;
equal(install.data.version, newversion);
equal(install.reason, reason);
equal(install.data.oldVersion, "2.0");
let startup = await onStartup;
equal(startup.data.version, newversion);
equal(startup.reason, reason);
equal(startup.data.oldVersion, "2.0");
addon = await promiseAddonByID(ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, newversion);
do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
// Now restart, the temporary addon will go away which should
// be the opposite action (ie, if the temporary addon was an
// upgrade, then removing it is a downgrade and vice versa)
reason = reason == BOOTSTRAP_REASONS.ADDON_UPGRADE ?
BOOTSTRAP_REASONS.ADDON_DOWNGRADE :
BOOTSTRAP_REASONS.ADDON_UPGRADE;
onShutdown = waitForBootstrapEvent("shutdown", ID);
onUninstall = waitForBootstrapEvent("uninstall", ID);
onInstall = waitForBootstrapEvent("install", ID);
onStartup = waitForBootstrapEvent("startup", ID);
await promiseRestartManager();
shutdown = await onShutdown;
equal(shutdown.data.version, newversion);
equal(shutdown.reason, reason);
uninstall = await onUninstall;
equal(uninstall.data.version, newversion);
equal(uninstall.reason, reason);
install = await onInstall;
equal(install.data.version, "2.0");
equal(install.reason, reason);
equal(install.data.oldVersion, newversion);
startup = await onStartup;
equal(startup.data.version, "2.0");
// We don't actually propagate the upgrade/downgrade reason across
// the browser restart when a temporary addon is removed. See
// bug 1359558 for detailed reasoning.
equal(startup.reason, BOOTSTRAP_REASONS.APP_STARTUP);
BootstrapMonitor.checkAddonInstalled(ID, "2.0");
BootstrapMonitor.checkAddonStarted(ID, "2.0");
addon = await promiseAddonByID(ID);
// existing add-on is back
do_check_neq(addon, null);
do_check_eq(addon.version, "2.0");
do_check_eq(addon.name, "Test Bootstrap 1");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
target.remove(true);
}
}
// remove original add-on
@ -324,6 +285,75 @@ add_task(async function() {
await promiseRestartManager();
});
// Test that loading from the same path multiple times work
add_task(async function test_samefile() {
// File locking issues on Windows, ugh
if (AppConstants.platform == "win") {
return;
}
// test that a webextension works
let webext = createTempWebExtensionFile({
manifest: {
version: "1.0",
name: "Test WebExtension 1 (temporary)",
applications: {
gecko: {
id: ID
}
}
}
});
await Promise.all([
AddonManager.installTemporaryAddon(webext),
promiseWebExtensionStartup(),
]);
let addon = await promiseAddonByID(ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "1.0");
do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
webext.remove(false);
webext = createTempWebExtensionFile({
manifest: {
version: "2.0",
name: "Test WebExtension 1 (temporary)",
applications: {
gecko: {
id: ID
}
}
}
});
await Promise.all([
AddonManager.installTemporaryAddon(webext),
promiseWebExtensionStartup(),
]);
addon = await promiseAddonByID(ID);
// temporary add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.version, "2.0");
do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
do_check_true(addon.isCompatible);
do_check_false(addon.appDisabled);
do_check_true(addon.isActive);
do_check_eq(addon.type, "extension");
do_check_true(addon.isWebExtension);
do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
addon.uninstall();
});
// Install a temporary add-on over the top of an existing add-on.
// Uninstall it and make sure the existing add-on comes back.
add_task(async function() {