Bug 1209341 - allow loading unsigned restartless add-ons at runtime. r=mossop

This commit is contained in:
Robert Helmer 2015-11-03 10:07:08 -08:00
parent 8db12ffec2
commit a2347b9b5b
7 changed files with 602 additions and 4 deletions

View File

@ -2315,6 +2315,27 @@ var AddonManagerInternal = {
}
},
/**
* Starts installation of a temporary add-on from a local directory.
* @param aDirectory
* The directory of the add-on to be temporarily installed
* @return a Promise that rejects if the add-on is not restartless
* or an add-on with the same ID is already temporarily installed
*/
installTemporaryAddon: function AMI_installTemporaryAddon(aFile) {
if (!gStarted)
throw Components.Exception("AddonManager is not initialized",
Cr.NS_ERROR_NOT_INITIALIZED);
if (!(aFile instanceof Ci.nsIFile))
throw Components.Exception("aFile must be a nsIFile",
Cr.NS_ERROR_INVALID_ARG);
return AddonManagerInternal._getProviderByName("XPIProvider")
.installTemporaryAddon(aFile);
},
/**
* Gets an icon from the icon set provided by the add-on
* that is closest to the specified size.
@ -3026,8 +3047,10 @@ this.AddonManager = {
SCOPE_APPLICATION: 4,
// Installed for all users of the computer.
SCOPE_SYSTEM: 8,
// Installed temporarily
SCOPE_TEMPORARY: 16,
// The combination of all scopes.
SCOPE_ALL: 15,
SCOPE_ALL: 31,
// 1-15 are different built-in views for the add-on type
VIEW_TYPE_LIST: "list",
@ -3207,6 +3230,10 @@ this.AddonManager = {
aInstalls);
},
installTemporaryAddon: function AM_installTemporaryAddon(aDirectory) {
return AddonManagerInternal.installTemporaryAddon(aDirectory);
},
addManagerListener: function AM_addManagerListener(aListener) {
AddonManagerInternal.addManagerListener(aListener);
},

View File

@ -202,6 +202,9 @@ function loadView(aViewId) {
}
function isCorrectlySigned(aAddon) {
// temporary add-ons do not require signing
if (aAddon.scope == AddonManager.SCOPE_TEMPORARY)
return true;
if (aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING)
return false;
if (aAddon.foreignInstall && aAddon.signedState < AddonManager.SIGNEDSTATE_SIGNED)

View File

@ -146,6 +146,7 @@ const KEY_APP_GLOBAL = "app-global";
const KEY_APP_SYSTEM_LOCAL = "app-system-local";
const KEY_APP_SYSTEM_SHARE = "app-system-share";
const KEY_APP_SYSTEM_USER = "app-system-user";
const KEY_APP_TEMPORARY = "app-temporary";
const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
const XPI_PERMISSION = "install";
@ -646,6 +647,12 @@ function canRunInSafeMode(aAddon) {
// Even though the updated system add-ons aren't generally run in safe mode we
// include them here so their uninstall functions get called when switching
// back to the default set.
// TODO product should make the call about temporary add-ons running
// in safe mode. assuming for now that they are.
if (aAddon._installLocation.name == KEY_APP_TEMPORARY)
return true;
return aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS ||
aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS;
}
@ -666,8 +673,10 @@ function isUsableAddon(aAddon) {
aAddon.signedState != AddonManager.SIGNEDSTATE_SYSTEM) {
return false;
}
if (aAddon._installLocation.name != KEY_APP_SYSTEM_DEFAULTS && mustSign(aAddon.type)) {
// temporary and system add-ons do not require signing
if ((aAddon._installLocation.name != KEY_APP_SYSTEM_DEFAULTS &&
aAddon._installLocation.name != KEY_APP_TEMPORARY) &&
mustSign(aAddon.type)) {
if (aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING)
return false;
if (aAddon.foreignInstall && aAddon.signedState < AddonManager.SIGNEDSTATE_SIGNED)
@ -2544,6 +2553,11 @@ this.XPIProvider = {
// These must be in order of priority, highest to lowest,
// for processFileChanges etc. to work
XPIProvider.installLocations.push(TemporaryInstallLocation);
XPIProvider.installLocationsByName[TemporaryInstallLocation.name] =
TemporaryInstallLocation;
// The profile location is always enabled
addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
[DIR_EXTENSIONS],
@ -3784,6 +3798,95 @@ this.XPIProvider = {
}, aFile);
},
/**
* Temporarily installs add-on from local directory.
* As this is intended for development, the signature is not checked and
* the add-on does not persist on application restart.
*
* @param aDirectory
* The directory containing the unpacked add-on directory or XPI file
*
* @return a Promise that rejects if the add-on is not restartless
* or an add-on with the same ID is already temporarily installed
*/
installTemporaryAddon: Task.async(function*
XPI_installTemporaryAddon(aDirectory) {
let addon = yield loadManifestFromFile(aDirectory, TemporaryInstallLocation);
if (!addon.bootstrap) {
throw new Error("Only restartless (bootstrap) add-ons"
+ " can be temporarily installed:", addon.id);
}
let oldAddon = yield new Promise(
resolve => XPIDatabase.getVisibleAddonForID(addon.id, resolve));
if (oldAddon) {
if (oldAddon.location == KEY_APP_TEMPORARY) {
logger.warn("temporary add-on already installed:", addon.id);
throw new Error("Add-on with ID " + oldAddon.id + " is already"
+ " temporarily installed");
}
else if (!oldAddon.bootstrap) {
logger.warn("Non-restartless Add-on is already installed", addon.id);
throw new Error("Non-restartless add-on with ID "
+ oldAddon.id + " is already installed");
}
else {
logger.warn("Addon with ID " + oldAddon.id + " already installed,"
+ " older version will be disabled");
let existingAddonID = oldAddon.id;
let existingAddon = oldAddon._sourceBundle;
// We'll be replacing a currently active bootstrapped add-on so
// call its uninstall method
let newVersion = addon.version;
let oldVersion = oldAddon.version;
let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
BOOTSTRAP_REASONS.ADDON_UPGRADE :
BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
if (oldAddon.active) {
XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
"shutdown", uninstallReason,
{ newVersion });
}
this.callBootstrapMethod(oldAddon, existingAddon,
"uninstall", uninstallReason, { newVersion });
this.unloadBootstrapScope(existingAddonID);
flushStartupCache();
}
}
let file = addon._sourceBundle;
let wrapper = createWrapper(addon);
let oldWrapper = createWrapper(oldAddon);
XPIProvider.callBootstrapMethod(addon, file, "install",
BOOTSTRAP_REASONS.ADDON_INSTALL);
addon.state = AddonManager.STATE_INSTALLED;
logger.debug("Install of temporary addon in " + aDirectory.path + " completed.");
addon.visible = true;
addon.enabled = true;
addon.active = true;
addon = XPIDatabase.addAddonMetadata(addon, file.persistentDescriptor);
XPIStates.addAddon(addon);
XPIDatabase.saveChanges();
// addon was modified above, create new wrapper
wrapper = createWrapper(addon);
AddonManagerPrivate.callAddonListeners("onInstalling", wrapper,
false);
XPIProvider.callBootstrapMethod(addon, file, "startup",
BOOTSTRAP_REASONS.ADDON_ENABLE);
AddonManagerPrivate.callInstallListeners("onExternalInstall",
null, wrapper, oldWrapper,
false);
AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
}),
/**
* Removes an AddonInstall from the list of active installs.
*
@ -6988,6 +7091,9 @@ function AddonWrapper(aAddon) {
});
this.__defineGetter__("hidden", function AddonWrapper_hidden() {
if (aAddon._installLocation.name == KEY_APP_TEMPORARY)
return false;
return (aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS ||
aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS);
});
@ -7818,6 +7924,21 @@ Object.assign(SystemAddonInstallLocation.prototype, {
}),
});
/**
* An object which identifies a directory install location for temporary
* add-ons.
*/
const TemporaryInstallLocation = {
locked: false,
name: KEY_APP_TEMPORARY,
scope: AddonManager.SCOPE_TEMPORARY,
getAddonLocations: () => [],
isLinkedAddon: () => false,
installAddon: () => {},
uninstallAddon: (aAddon) => {},
getStagingDir: () => {},
}
#ifdef XP_WIN
/**
* An object that identifies a registry install location for add-ons. The location

View File

@ -323,7 +323,7 @@ function DBAddonInternal(aLoaded) {
if (aLoaded._installLocation) {
this._installLocation = aLoaded._installLocation;
this.location = aLoaded._installLocation._name;
this.location = aLoaded._installLocation.name;
}
else if (aLoaded.location) {
this._installLocation = XPIProvider.installLocationsByName[this.location];

View File

@ -0,0 +1 @@
Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);

View File

@ -0,0 +1,445 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const ID = "bootstrap1@tests.mozilla.org";
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
startupManager();
BootstrapMonitor.init();
// Install a temporary add-on with no existing add-on present.
// Restart and make sure it has gone away.
add_task(function*() {
let extInstallCalled = false;
AddonManager.addInstallListener({
onExternalInstall: (aInstall) => {
do_check_eq(aInstall.id, ID);
do_check_eq(aInstall.version, "1.0");
extInstallCalled = true;
},
});
let installingCalled = false;
let installedCalled = false;
AddonManager.addAddonListener({
onInstalling: (aInstall) => {
do_check_eq(aInstall.id, ID);
do_check_eq(aInstall.version, "1.0");
installingCalled = true;
},
onInstalled: (aInstall) => {
do_check_eq(aInstall.id, ID);
do_check_eq(aInstall.version, "1.0");
installedCalled = true;
},
onInstallStarted: (aInstall) => {
do_throw("onInstallStarted called unexpectedly");
}
});
yield AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap1_1"));
do_check_true(extInstallCalled);
do_check_true(installingCalled);
do_check_true(installedCalled);
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
let addon = yield promiseAddonByID(ID);
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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
yield promiseRestartManager();
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(ID);
addon = yield promiseAddonByID(ID);
do_check_eq(addon, null);
yield promiseRestartManager();
});
// 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(function*() {
yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
let addon = yield promiseAddonByID(ID);
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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
// test that an unpacked add-on works too
let tempdir = gTmpD.clone();
writeInstallRDFToDir({
id: ID,
version: "2.0",
bootstrap: true,
unpack: true,
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test Bootstrap 1 (temporary)",
}, tempdir, "bootstrap1@tests.mozilla.org", "bootstrap.js");
let unpacked_addon = tempdir.clone();
unpacked_addon.append(ID);
do_get_file("data/test_temporary/bootstrap.js")
.copyTo(unpacked_addon, "bootstrap.js");
yield AddonManager.installTemporaryAddon(unpacked_addon);
BootstrapMonitor.checkAddonInstalled(ID, "2.0");
BootstrapMonitor.checkAddonStarted(ID, "2.0");
addon = yield 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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
restartManager();
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
addon = yield 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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
unpacked_addon.remove(true);
addon.uninstall();
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(ID);
yield promiseRestartManager();
});
// 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(function*() {
yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
let tempdir = gTmpD.clone();
writeInstallRDFToDir({
id: ID,
version: "2.0",
bootstrap: true,
unpack: true,
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test Bootstrap 1 (temporary)",
}, tempdir);
let unpacked_addon = tempdir.clone();
unpacked_addon.append(ID);
let extInstallCalled = false;
AddonManager.addInstallListener({
onExternalInstall: (aInstall) => {
do_check_eq(aInstall.id, ID);
do_check_eq(aInstall.version, "2.0");
extInstallCalled = true;
},
});
let installingCalled = false;
let installedCalled = false;
AddonManager.addAddonListener({
onInstalling: (aInstall) => {
do_check_eq(aInstall.id, ID);
if (!installingCalled)
do_check_eq(aInstall.version, "2.0");
installingCalled = true;
},
onInstalled: (aInstall) => {
do_check_eq(aInstall.id, ID);
if (!installedCalled)
do_check_eq(aInstall.version, "2.0");
installedCalled = true;
},
onInstallStarted: (aInstall) => {
do_throw("onInstallStarted called unexpectedly");
}
});
yield AddonManager.installTemporaryAddon(unpacked_addon);
do_check_true(extInstallCalled);
do_check_true(installingCalled);
do_check_true(installedCalled);
let addon = yield promiseAddonByID(ID);
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
addon.uninstall();
addon = yield promiseAddonByID(ID);
BootstrapMonitor.checkAddonInstalled(ID);
BootstrapMonitor.checkAddonStarted(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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
unpacked_addon.remove(true);
addon.uninstall();
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(ID);
yield promiseRestartManager();
});
// Install a temporary add-on over the top of an existing disabled add-on.
// After restart, the existing add-on should continue to be installed and disabled.
add_task(function*() {
yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
let addon = yield promiseAddonByID(ID);
addon.userDisabled = true;
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonNotStarted(ID);
let tempdir = gTmpD.clone();
writeInstallRDFToDir({
id: ID,
version: "2.0",
bootstrap: true,
unpack: true,
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test Bootstrap 1 (temporary)",
}, tempdir, "bootstrap1@tests.mozilla.org", "bootstrap.js");
let unpacked_addon = tempdir.clone();
unpacked_addon.append(ID);
do_get_file("data/test_temporary/bootstrap.js")
.copyTo(unpacked_addon, "bootstrap.js");
let extInstallCalled = false;
AddonManager.addInstallListener({
onExternalInstall: (aInstall) => {
do_check_eq(aInstall.id, ID);
do_check_eq(aInstall.version, "2.0");
extInstallCalled = true;
},
});
yield AddonManager.installTemporaryAddon(unpacked_addon);
do_check_true(extInstallCalled);
let tempAddon = yield promiseAddonByID(ID);
BootstrapMonitor.checkAddonInstalled(ID, "2.0");
BootstrapMonitor.checkAddonStarted(ID);
// temporary add-on is installed and started
do_check_neq(tempAddon, null);
do_check_eq(tempAddon.version, "2.0");
do_check_eq(tempAddon.name, "Test Bootstrap 1 (temporary)");
do_check_true(tempAddon.isCompatible);
do_check_false(tempAddon.appDisabled);
do_check_true(tempAddon.isActive);
do_check_eq(tempAddon.type, "extension");
do_check_eq(tempAddon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
tempAddon.uninstall();
unpacked_addon.remove(true);
addon.userDisabled = false;
addon = yield promiseAddonByID(ID);
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
addon.uninstall();
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(ID);
yield promiseRestartManager();
});
// Installing a temporary add-on over a non-restartless add-on should fail.
add_task(function*(){
yield promiseInstallAllFiles([do_get_addon("test_install1")], true);
let non_restartless_ID = "addon1@tests.mozilla.org";
BootstrapMonitor.checkAddonNotInstalled(non_restartless_ID);
BootstrapMonitor.checkAddonNotStarted(non_restartless_ID);
restartManager();
BootstrapMonitor.checkAddonNotInstalled(non_restartless_ID);
BootstrapMonitor.checkAddonNotStarted(non_restartless_ID);
let addon = yield promiseAddonByID(non_restartless_ID);
// non-restartless add-on is installed and started
do_check_neq(addon, null);
do_check_eq(addon.id, non_restartless_ID);
do_check_eq(addon.version, "1.0");
do_check_eq(addon.name, "Test 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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
let tempdir = gTmpD.clone();
writeInstallRDFToDir({
id: non_restartless_ID,
version: "2.0",
bootstrap: true,
unpack: true,
targetApplications: [{
id: "xpcshell@tests.mozilla.org",
minVersion: "1",
maxVersion: "1"
}],
name: "Test 1 (temporary)",
}, tempdir);
let unpacked_addon = tempdir.clone();
unpacked_addon.append(non_restartless_ID);
try {
yield AddonManager.installTemporaryAddon(unpacked_addon);
do_throw("Installing over a non-restartless add-on should return"
+ " a rejected promise");
} catch (err) {
do_check_eq(err.message,
"Non-restartless add-on with ID addon1@tests.mozilla.org is"
+ " already installed");
}
unpacked_addon.remove(true);
addon.uninstall();
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(ID);
yield promiseRestartManager();
});
// Installing a temporary add-on when there is already a temporary
// add-on should fail.
add_task(function*() {
yield AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap1_1"));
let addon = yield promiseAddonByID(ID);
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
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_MISSING : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
try {
yield AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap1_1"));
do_throw("Installing a temporary second temporary add-on should return"
+ " a rejected promise");
} catch (err) {
do_check_eq(err.message,
"Add-on with ID bootstrap1@tests.mozilla.org is already temporarily"
+ " installed");
}
BootstrapMonitor.checkAddonInstalled(ID, "1.0");
BootstrapMonitor.checkAddonStarted(ID, "1.0");
yield promiseRestartManager();
BootstrapMonitor.checkAddonNotInstalled(ID);
BootstrapMonitor.checkAddonNotStarted(ID);
});

View File

@ -29,6 +29,7 @@ skip-if = appname != "firefox"
[test_system_reset.js]
[test_XPIcancel.js]
[test_XPIStates.js]
[test_temporary.js]