--- a/devtools/server/tests/unit/test_addon_reload.js
+++ b/devtools/server/tests/unit/test_addon_reload.js
@@ -14,53 +14,73 @@ function promiseAddonEvent(event) {
resolve(args);
}
};
AddonManager.addAddonListener(listener);
});
}
+function promiseWebExtensionStartup() {
+ const {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+ return new Promise(resolve => {
+ let listener = (evt, extension) => {
+ Management.off("ready", listener);
+ resolve(extension);
+ };
+
+ Management.on("ready", listener);
+ });
+}
+
function* findAddonInRootList(client, addonId) {
const result = yield client.listAddons();
const addonActor = result.addons.filter(addon => addon.id === addonId)[0];
ok(addonActor, `Found add-on actor for ${addonId}`);
return addonActor;
}
-function* reloadAddon(client, addonActor) {
+async function reloadAddon(client, addonActor) {
// The add-on will be re-installed after a successful reload.
const onInstalled = promiseAddonEvent("onInstalled");
- yield client.request({to: addonActor.actor, type: "reload"});
- yield onInstalled;
+ await client.request({to: addonActor.actor, type: "reload"});
+ await onInstalled;
}
function getSupportFile(path) {
const allowMissing = false;
return do_get_file(path, allowMissing);
}
add_task(function* testReloadExitedAddon() {
const client = yield new Promise(resolve => {
get_chrome_actors(client => resolve(client));
});
// Install our main add-on to trigger reloads on.
const addonFile = getSupportFile("addons/web-extension");
- const installedAddon = yield AddonManager.installTemporaryAddon(
- addonFile);
+ const [installedAddon] = yield Promise.all([
+ AddonManager.installTemporaryAddon(addonFile),
+ promiseWebExtensionStartup(),
+ ]);
// Install a decoy add-on.
const addonFile2 = getSupportFile("addons/web-extension2");
- const installedAddon2 = yield AddonManager.installTemporaryAddon(
- addonFile2);
+ const [installedAddon2] = yield Promise.all([
+ AddonManager.installTemporaryAddon(addonFile2),
+ promiseWebExtensionStartup(),
+ ]);
let addonActor = yield findAddonInRootList(client, installedAddon.id);
- yield reloadAddon(client, addonActor);
+ yield Promise.all([
+ reloadAddon(client, addonActor),
+ promiseWebExtensionStartup(),
+ ]);
// Uninstall the decoy add-on, which should cause its actor to exit.
const onUninstalled = promiseAddonEvent("onUninstalled");
installedAddon2.uninstall();
const [uninstalledAddon] = yield onUninstalled;
// Try to re-list all add-ons after a reload.
// This was throwing an exception because of the exited actor.
@@ -74,18 +94,20 @@ add_task(function* testReloadExitedAddon
client.addListener("addonListChanged", function listener() {
client.removeListener("addonListChanged", listener);
resolve();
});
});
// Install an upgrade version of the first add-on.
const addonUpgradeFile = getSupportFile("addons/web-extension-upgrade");
- const upgradedAddon = yield AddonManager.installTemporaryAddon(
- addonUpgradeFile);
+ const [upgradedAddon] = yield Promise.all([
+ AddonManager.installTemporaryAddon(addonUpgradeFile),
+ promiseWebExtensionStartup(),
+ ]);
// Waiting for addonListChanged unsolicited event
yield onAddonListChanged;
// re-list all add-ons after an upgrade.
const upgradedAddonActor = yield findAddonInRootList(client, upgradedAddon.id);
equal(upgradedAddonActor.id, addonActor.id);
// The actor id should be the same after the upgrade.
--- a/testing/specialpowers/content/SpecialPowersObserverAPI.js
+++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js
@@ -545,26 +545,26 @@ SpecialPowersObserverAPI.prototype = {
extension.on("startup", () => {
this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionSetId", args: [extension.id]});
});
// Make sure the extension passes the packaging checks when
// they're run on a bare archive rather than a running instance,
// as the add-on manager runs them.
let extensionData = new ExtensionData(extension.rootURI);
- extensionData.readManifest().then(
+ extensionData.loadManifest().then(
() => {
return extensionData.initAllLocales().then(() => {
if (extensionData.errors.length) {
return Promise.reject("Extension contains packaging errors");
}
});
},
() => {
- // readManifest() will throw if we're loading an embedded
+ // loadManifest() will throw if we're loading an embedded
// extension, so don't worry about locale errors in that
// case.
}
).then(() => {
return extension.startup();
}).then(() => {
this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionStarted", args: []});
}).catch(e => {
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -78,16 +78,17 @@ var {
GlobalManager,
ParentAPIManager,
apiManager: Management,
} = ExtensionParent;
const {
EventEmitter,
LocaleData,
+ StartupCache,
getUniqueId,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
const LOGGER_ID_BASE = "addons.webextension.";
const UUID_MAP_PREF = "extensions.webextensions.uuids";
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
@@ -212,17 +213,17 @@ UninstallObserver.init();
// Represents the data contained in an extension, contained either
// in a directory or a zip file, which may or may not be installed.
// This class implements the functionality of the Extension class,
// primarily related to manifest parsing and localization, which is
// useful prior to extension installation or initialization.
//
// No functionality of this class is guaranteed to work before
-// |readManifest| has been called, and completed.
+// |loadManifest| has been called, and completed.
this.ExtensionData = class {
constructor(rootURI) {
this.rootURI = rootURI;
this.manifest = null;
this.id = null;
this.uuid = null;
this.localeData = null;
@@ -396,19 +397,17 @@ this.ExtensionData = class {
// a *.domain.com to specific-host.domain.com that's actually a
// drop in permissions but the simple test below will cause a prompt.
return {
hosts: newPermissions.hosts.filter(perm => !oldPermissions.hosts.includes(perm)),
permissions: newPermissions.permissions.filter(perm => !oldPermissions.permissions.includes(perm)),
};
}
- // Reads the extension's |manifest.json| file, and stores its
- // parsed contents in |this.manifest|.
- readManifest() {
+ parseManifest() {
return Promise.all([
this.readJSON("manifest.json"),
Management.lazyInit(),
]).then(([manifest]) => {
this.manifest = manifest;
this.rawManifest = manifest;
if (manifest && manifest.default_locale) {
@@ -430,60 +429,64 @@ this.ExtensionData = class {
if (this.localeData) {
context.preprocessors.localize = (value, context) => this.localize(value);
}
let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
if (normalized.error) {
this.manifestError(normalized.error);
} else {
- this.manifest = normalized.value;
+ return normalized.value;
}
+ });
+ }
+
+ // Reads the extension's |manifest.json| file, and stores its
+ // parsed contents in |this.manifest|.
+ async loadManifest() {
+ [this.manifest] = await Promise.all([
+ this.parseManifest(),
+ Management.lazyInit(),
+ ]);
- try {
- // Do not override the add-on id that has been already assigned.
- if (!this.id && this.manifest.applications.gecko.id) {
- this.id = this.manifest.applications.gecko.id;
- }
- } catch (e) {
- // Errors are handled by the type checks above.
+ if (!this.manifest) {
+ return;
+ }
+
+ try {
+ // Do not override the add-on id that has been already assigned.
+ if (!this.id && this.manifest.applications.gecko.id) {
+ this.id = this.manifest.applications.gecko.id;
}
+ } catch (e) {
+ // Errors are handled by the type checks above.
+ }
- let containersEnabled = true;
- try {
- containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
- } catch (e) {
- // If we fail here, we are in some xpcshell test.
+ let whitelist = [];
+ for (let perm of this.manifest.permissions) {
+ if (perm == "contextualIdentities" && !Preferences.get("privacy.userContext.enabled")) {
+ continue;
}
- let permissions = this.manifest.permissions || [];
-
- let whitelist = [];
- for (let perm of permissions) {
- if (perm == "contextualIdentities" && !containersEnabled) {
- continue;
- }
-
- this.permissions.add(perm);
+ this.permissions.add(perm);
- let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
- if (!match) {
- whitelist.push(perm);
- } else if (match[1] == "experiments" && match[2]) {
- this.apiNames.add(match[2]);
- }
+ let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
+ if (!match) {
+ whitelist.push(perm);
+ } else if (match[1] == "experiments" && match[2]) {
+ this.apiNames.add(match[2]);
}
- this.whiteListedHosts = new MatchPattern(whitelist);
+ }
+ this.whiteListedHosts = new MatchPattern(whitelist);
- for (let api of this.apiNames) {
- this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
- }
+ for (let api of this.apiNames) {
+ this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
+ }
- return this.manifest;
- });
+ return this.manifest;
}
localizeMessage(...args) {
return this.localeData.localizeMessage(...args);
}
localize(...args) {
return this.localeData.localize(...args);
@@ -632,16 +635,20 @@ this.Extension = class extends Extension
Services.obs.addObserver(this, "xpcom-shutdown", false);
this.cleanupFile = addonData.cleanupFile || null;
delete addonData.cleanupFile;
}
this.addonData = addonData;
this.startupReason = startupReason;
+ if (["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)) {
+ StartupCache.clearAddonData(addonData.id);
+ }
+
this.remote = ExtensionManagement.useRemoteWebExtensions;
if (this.remote && processCount !== 1) {
throw new Error("Out-of-process WebExtensions are not supported with multiple child processes");
}
// This is filled in the first time an extension child is created.
this.parentMessageManager = null;
@@ -716,18 +723,35 @@ this.Extension = class extends Extension
// Checks that the given URL is a child of our baseURI.
isExtensionURL(url) {
let uri = Services.io.newURI(url);
let common = this.baseURI.getCommonBaseSpec(uri);
return common == this.baseURI.spec;
}
- readManifest() {
- return super.readManifest().then(manifest => {
+ readLocaleFile(locale) {
+ return StartupCache.locales.get([this.id, locale],
+ () => super.readLocaleFile(locale))
+ .then(result => {
+ this.localeData.messages.set(locale, result);
+ });
+ }
+
+ parseManifest() {
+ return StartupCache.manifests.get([this.id, Locale.getLocale()],
+ () => super.parseManifest());
+ }
+
+ loadManifest() {
+ return super.loadManifest().then(manifest => {
+ if (this.errors.length) {
+ return Promise.reject({errors: this.errors});
+ }
+
if (AppConstants.RELEASE_OR_BETA) {
return manifest;
}
// Load Experiments APIs that this extension depends on.
return Promise.all(
Array.from(this.apiNames, api => ExtensionAPIs.load(api))
).then(apis => {
@@ -834,39 +858,34 @@ this.Extension = class extends Extension
return new Map([
["@@extension_id", this.uuid],
]);
}
// Reads the locale file for the given Gecko-compatible locale code, or if
// no locale is given, the available locale closest to the UI locale.
// Sets the currently selected locale on success.
- initLocale(locale = undefined) {
- // Ugh.
- let super_ = super.initLocale.bind(this);
-
- return Task.spawn(function* () {
- if (locale === undefined) {
- let locales = yield this.promiseLocales();
+ async initLocale(locale = undefined) {
+ if (locale === undefined) {
+ let locales = await this.promiseLocales();
- let localeList = Array.from(locales.keys(), locale => {
- return {name: locale, locales: [locale]};
- });
+ let localeList = Array.from(locales.keys(), locale => {
+ return {name: locale, locales: [locale]};
+ });
- let match = Locale.findClosestLocale(localeList);
- locale = match ? match.name : this.defaultLocale;
- }
+ let match = Locale.findClosestLocale(localeList);
+ locale = match ? match.name : this.defaultLocale;
+ }
- return super_(locale);
- }.bind(this));
+ return super.initLocale(locale);
}
startup() {
let started = false;
- return this.readManifest().then(() => {
+ return this.loadManifest().then(() => {
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
started = true;
if (!this.hasShutdown) {
return this.initLocale();
}
}).then(() => {
if (this.errors.length) {
@@ -921,16 +940,21 @@ this.Extension = class extends Extension
file.remove(false);
}).catch(Cu.reportError);
}
shutdown(reason) {
this.shutdownReason = reason;
this.hasShutdown = true;
+ if (this.cleanupFile ||
+ ["ADDON_INSTALL", "ADDON_UNINSTALL", "ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(reason)) {
+ StartupCache.clearAddonData(this.id);
+ }
+
Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
if (!this.manifest) {
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
return;
}
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -311,17 +311,27 @@ function getAPILevelForWindow(window, ad
// (see Bug 1214658 for rationale)
return CONTENTSCRIPT_PRIVILEGES;
}
// WebExtension URLs loaded into top frames UI could have full API level privileges.
return FULL_PRIVILEGES;
}
+let cacheInvalidated = 0;
+function onCacheInvalidate() {
+ cacheInvalidated++;
+}
+Services.obs.addObserver(onCacheInvalidate, "startupcache-invalidate", false);
+
ExtensionManagement = {
+ get cacheInvalidated() {
+ return cacheInvalidated;
+ },
+
get isExtensionProcess() {
if (this.useRemoteWebExtensions) {
return Services.appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE;
}
return Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
},
startupExtension: Service.startupExtension.bind(Service),
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -17,16 +17,20 @@ Cu.import("resource://gre/modules/Servic
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
"resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+ "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB",
+ "resource://gre/modules/IndexedDB.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
"resource:///modules/translation/LanguageDetector.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
"resource://gre/modules/Locale.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
@@ -50,16 +54,128 @@ XPCOMUtils.defineLazyGetter(this, "conso
let nextId = 0;
XPCOMUtils.defineLazyGetter(this, "uniqueProcessID", () => Services.appinfo.uniqueProcessID);
function getUniqueId() {
return `${nextId++}-${uniqueProcessID}`;
}
+let StartupCache = {
+ DB_NAME: "ExtensionStartupCache",
+
+ SCHEMA_VERSION: 1,
+
+ STORE_NAMES: Object.freeze(["locales", "manifests", "schemas"]),
+
+ dbPromise: null,
+
+ cacheInvalidated: 0,
+
+ initDB(db) {
+ for (let name of StartupCache.STORE_NAMES) {
+ try {
+ db.deleteObjectStore(name);
+ } catch (e) {
+ // Don't worry if the store doesn't already exist.
+ }
+ db.createObjectStore(name);
+ }
+ },
+
+ clearAddonData(id) {
+ let range = IDBKeyRange.bound([id], [id, "\uFFFF"]);
+
+ return Promise.all([
+ this.locales.delete(range),
+ this.manifests.delete(range),
+ ]).catch(e => {
+ // Ignore the error. It happens when we try to flush the add-on
+ // data after the AddonManager has flushed the entire startup cache.
+ });
+ },
+
+ async reallyOpen(invalidate = false) {
+ if (this.dbPromise) {
+ let db = await this.dbPromise;
+ db.close();
+ }
+
+ if (invalidate) {
+ this.cacheInvalidated = ExtensionManagement.cacheInvalidated;
+
+ if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ IndexedDB.deleteDatabase(this.DB_NAME, {storage: "persistent"});
+ }
+ }
+
+ return IndexedDB.open(this.DB_NAME,
+ {storage: "persistent", version: this.SCHEMA_VERSION},
+ db => this.initDB(db));
+ },
+
+ async open() {
+ if (ExtensionManagement.cacheInvalidated > this.cacheInvalidated) {
+ this.dbPromise = this.reallyOpen(true);
+ } else if (!this.dbPromise) {
+ this.dbPromise = this.reallyOpen();
+ }
+
+ return this.dbPromise;
+ },
+
+ observe(subject, topic, data) {
+ if (topic === "startupcache-invalidate") {
+ this.dbPromise = this.reallyOpen(true).catch(e => {});
+ }
+ },
+};
+
+Services.obs.addObserver(StartupCache, "startupcache-invalidate", false);
+
+class CacheStore {
+ constructor(storeName) {
+ this.storeName = storeName;
+ }
+
+ async get(key, createFunc) {
+ let db;
+ let value;
+ try {
+ db = await StartupCache.open();
+
+ value = await db.objectStore(this.storeName)
+ .get(key);
+ } catch (e) {
+ Cu.reportError(e);
+
+ return createFunc(key);
+ }
+
+ if (value === undefined) {
+ value = await createFunc(key);
+
+ db.objectStore(this.storeName, "readwrite")
+ .put(value, key);
+ }
+
+ return value;
+ }
+
+ async delete(key) {
+ let db = await StartupCache.open();
+
+ return db.objectStore(this.storeName, "readwrite").delete(key);
+ }
+}
+
+for (let name of StartupCache.STORE_NAMES) {
+ StartupCache[name] = new CacheStore(name);
+}
+
/**
* An Error subclass for which complete error messages are always passed
* to extensions, rather than being interpreted as an unknown error.
*/
class ExtensionError extends Error {}
function filterStack(error) {
return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
@@ -1192,11 +1308,12 @@ this.ExtensionUtils = {
EventEmitter,
ExtensionError,
IconDetails,
LimitedSet,
LocaleData,
MessageManagerProxy,
SingletonEventManager,
SpreadArgs,
+ StartupCache,
};
XPCOMUtils.defineLazyGetter(this.ExtensionUtils, "PlatformInfo", PlatformInfo);
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -16,16 +16,17 @@ Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
DefaultMap,
+ StartupCache,
instanceOf,
} = ExtensionUtils;
XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
"@mozilla.org/addons/content-policy;1",
"nsIAddonContentPolicy");
this.EXPORTED_SYMBOLS = ["Schemas"];
@@ -2255,17 +2256,17 @@ this.Schemas = {
for (let namespace of json) {
this.getNamespace(namespace.namespace)
.addSchema(namespace);
}
},
load(url) {
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
- return readJSON(url).then(json => {
+ return StartupCache.schemas.get(url, readJSON).then(json => {
this.schemaJSON.set(url, json);
let data = Services.ppmm.initialProcessData;
data["Extension:Schemas"] = this.schemaJSON;
Services.ppmm.broadcastAsyncMessage("Schema:Add", {url, schema: json});
this.flushSchemas();
--- a/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
@@ -22,16 +22,16 @@ add_task(function* test_json_parser() {
"version": "0.1\\",
};
let fileURI = Services.io.newFileURI(xpi);
let uri = NetUtil.newURI(`jar:${fileURI.spec}!/`);
let extension = new ExtensionData(uri);
- yield extension.readManifest();
+ yield extension.parseManifest();
Assert.deepEqual(extension.rawManifest, expectedManifest,
"Manifest with correctly-filtered comments");
Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
xpi.remove(false);
});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_startup_cache.js
@@ -0,0 +1,110 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+const ADDON_ID = "test-startup-cache@xpcshell.mozilla.org";
+
+function makeExtension(opts) {
+ return {
+ useAddonManager: "permanent",
+
+ manifest: {
+ "version": opts.version,
+ "applications": {"gecko": {"id": ADDON_ID}},
+
+ "name": "__MSG_name__",
+
+ "default_locale": "en_US",
+ },
+
+ files: {
+ "_locales/en_US/messages.json": {
+ name: {
+ message: `en-US ${opts.version}`,
+ description: "Name.",
+ },
+ },
+ "_locales/fr/messages.json": {
+ name: {
+ message: `fr ${opts.version}`,
+ description: "Name.",
+ },
+ },
+ },
+
+ background() {
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "get-manifest") {
+ browser.test.sendMessage("manifest", browser.runtime.getManifest());
+ }
+ });
+ },
+ };
+}
+
+add_task(function* () {
+ Preferences.set("extensions.logging.enabled", false);
+ yield AddonTestUtils.promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension(
+ makeExtension({version: "1.0"}));
+
+ function getManifest() {
+ extension.sendMessage("get-manifest");
+ return extension.awaitMessage("manifest");
+ }
+
+
+ yield extension.startup();
+
+ equal(extension.version, "1.0", "Expected extension version");
+ let manifest = yield getManifest();
+ equal(manifest.name, "en-US 1.0", "Got expected manifest name");
+
+
+ do_print("Restart and re-check");
+ yield AddonTestUtils.promiseRestartManager();
+ yield extension.awaitStartup();
+
+ equal(extension.version, "1.0", "Expected extension version");
+ manifest = yield getManifest();
+ equal(manifest.name, "en-US 1.0", "Got expected manifest name");
+
+
+ do_print("Change locale to 'fr' and restart");
+ Preferences.set("general.useragent.locale", "fr");
+ yield AddonTestUtils.promiseRestartManager();
+ yield extension.awaitStartup();
+
+ equal(extension.version, "1.0", "Expected extension version");
+ manifest = yield getManifest();
+ equal(manifest.name, "fr 1.0", "Got expected manifest name");
+
+
+ do_print("Update to version 1.1");
+ yield extension.upgrade(makeExtension({version: "1.1"}));
+
+ equal(extension.version, "1.1", "Expected extension version");
+ manifest = yield getManifest();
+ equal(manifest.name, "fr 1.1", "Got expected manifest name");
+
+
+ do_print("Change locale to 'en-US' and restart");
+ Preferences.set("general.useragent.locale", "en-US");
+ yield AddonTestUtils.promiseRestartManager();
+ yield extension.awaitStartup();
+
+ equal(extension.version, "1.1", "Expected extension version");
+ manifest = yield getManifest();
+ equal(manifest.name, "en-US 1.1", "Got expected manifest name");
+
+
+ yield extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/test_locale_data.js
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_data.js
@@ -17,17 +17,17 @@ function* generateAddon(data) {
Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
xpi.remove(false);
});
let fileURI = Services.io.newFileURI(xpi);
let jarURI = NetUtil.newURI(`jar:${fileURI.spec}!/webextension/`);
let extension = new ExtensionData(jarURI);
- yield extension.readManifest();
+ yield extension.loadManifest();
return extension;
}
add_task(function* testMissingDefaultLocale() {
let extension = yield generateAddon({
"files": {
"_locales/en_US/messages.json": {},
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -63,16 +63,17 @@ skip-if = true # This test no longer tes
[test_ext_runtime_sendMessage_no_receiver.js]
[test_ext_runtime_sendMessage_self.js]
[test_ext_schemas.js]
[test_ext_schemas_api_injection.js]
[test_ext_schemas_async.js]
[test_ext_schemas_allowed_contexts.js]
[test_ext_shutdown_cleanup.js]
[test_ext_simple.js]
+[test_ext_startup_cache.js]
[test_ext_storage.js]
[test_ext_storage_sync.js]
head = head.js head_sync.js
skip-if = os == "android"
[test_ext_storage_sync_crypto.js]
skip-if = os == "android"
[test_ext_topSites.js]
skip-if = os == "android"
--- a/toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js
+++ b/toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js
@@ -6,25 +6,25 @@
/* exported startup, shutdown, install, uninstall */
Components.utils.import("resource://gre/modules/Extension.jsm");
var extension;
const BOOTSTRAP_REASON_TO_STRING_MAP = {
- 1: "APP_STARTUP",
- 2: "APP_SHUTDOWN",
- 3: "ADDON_ENABLE",
- 4: "ADDON_DISABLE",
- 5: "ADDON_INSTALL",
- 6: "ADDON_UNINSTALL",
- 7: "ADDON_UPGRADE",
- 8: "ADDON_DOWNGRADE",
-}
+ [this.APP_STARTUP]: "APP_STARTUP",
+ [this.APP_SHUTDOWN]: "APP_SHUTDOWN",
+ [this.ADDON_ENABLE]: "ADDON_ENABLE",
+ [this.ADDON_DISABLE]: "ADDON_DISABLE",
+ [this.ADDON_INSTALL]: "ADDON_INSTALL",
+ [this.ADDON_UNINSTALL]: "ADDON_UNINSTALL",
+ [this.ADDON_UPGRADE]: "ADDON_UPGRADE",
+ [this.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE",
+};
function install(data, reason) {
}
function startup(data, reason) {
extension = new Extension(data, BOOTSTRAP_REASON_TO_STRING_MAP[reason]);
extension.startup();
}
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -958,17 +958,17 @@ function getRDFProperty(aDs, aResource,
*/
var loadManifestFromWebManifest = Task.async(function*(aUri) {
// We're passed the URI for the manifest file. Get the URI for its
// parent directory.
let uri = NetUtil.newURI("./", null, aUri);
let extension = new ExtensionData(uri);
- let manifest = yield extension.readManifest();
+ let manifest = yield extension.loadManifest();
let theme = !!manifest.theme;
// Read the list of available locales, and pre-load messages for
// all locales.
let locales = yield extension.initAllLocales();
// If there were any errors loading the extension, bail out now.
if (extension.errors.length)
--- a/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
@@ -41,30 +41,49 @@ function* runTest(installer) {
button = mgrWindow.document.getElementById("detail-prefs-btn");
is_element_hidden(button, "Preferences button should not be visible");
yield close_manager(mgrWindow);
addon.uninstall();
}
+function promiseWebExtensionStartup() {
+ const {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+ return new Promise(resolve => {
+ let listener = (event, extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
// Test that deferred handling of optionsURL works for a signed webextension
add_task(function* test_options_signed() {
yield* runTest(function*() {
// The extension in-tree is signed with this ID:
const ID = "{9792932b-32b2-4567-998c-e7bf6c4c5e35}";
- yield install_addon("addons/options_signed.xpi");
+ yield Promise.all([
+ promiseWebExtensionStartup(),
+ install_addon("addons/options_signed.xpi"),
+ ]);
let addon = yield promiseAddonByID(ID);
return {addon, id: ID};
});
});
add_task(function* test_options_temporary() {
yield* runTest(function*() {
let dir = get_addon_file_url("options_signed").file;
- let addon = yield AddonManager.installTemporaryAddon(dir);
+ let [addon] = yield Promise.all([
+ AddonManager.installTemporaryAddon(dir),
+ promiseWebExtensionStartup(),
+ ]);
isnot(addon, null, "Extension is installed (temporarily)");
return {addon, id: addon.id};
});
});
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1068,17 +1068,17 @@ function promiseWebExtensionStartup() {
}
function promiseInstallWebExtension(aData) {
let addonFile = createTempWebExtensionFile(aData);
return promiseInstallAllFiles([addonFile]).then(installs => {
Services.obs.notifyObservers(addonFile, "flush-cache-entry", null);
// Since themes are disabled by default, it won't start up.
- if ("theme" in aData.manifest)
+ if (aData.manifest.theme)
return installs[0].addon;
return promiseWebExtensionStartup();
});
}
// By default use strict compatibility
Services.prefs.setBoolPref("extensions.strictCompatibility", true);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_reload.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js
@@ -75,17 +75,20 @@ add_task(function* test_reloading_a_temp
// This should be the last event called.
AddonManager.removeAddonListener(listener);
resolve();
},
}
AddonManager.addAddonListener(listener);
});
- yield addon.reload();
+ yield Promise.all([
+ addon.reload(),
+ promiseAddonStartup(),
+ ]);
yield onReload;
// Make sure reload() doesn't trigger uninstall events.
equal(receivedOnUninstalled, false, "reload should not trigger onUninstalled");
equal(receivedOnUninstalling, false, "reload should not trigger onUninstalling");
// Make sure reload() triggers install events, like an upgrade.
equal(receivedOnInstalling, true, "reload should trigger onInstalling");
@@ -106,17 +109,20 @@ add_task(function* test_can_reload_perma
disabledCalled = true
},
onEnabled: (aAddon) => {
do_check_true(disabledCalled);
enabledCalled = true
}
})
- yield addon.reload();
+ yield Promise.all([
+ addon.reload(),
+ promiseAddonStartup(),
+ ]);
do_check_true(disabledCalled);
do_check_true(enabledCalled);
notEqual(addon, null);
equal(addon.appDisabled, false);
equal(addon.userDisabled, false);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
@@ -31,21 +31,24 @@ function mapManifest(aPath, aManifestDat
function serveManifest(request, response) {
let manifest = gUpdateManifests[request.path];
response.setHeader("Content-Type", manifest.contentType, false);
response.write(manifest.data);
}
-
function promiseInstallWebExtension(aData) {
let addonFile = createTempWebExtensionFile(aData);
+ let startupPromise = promiseWebExtensionStartup();
+
return promiseInstallAllFiles([addonFile]).then(() => {
+ return startupPromise;
+ }).then(() => {
Services.obs.notifyObservers(addonFile, "flush-cache-entry", null);
return promiseAddonByID(aData.id);
});
}
var checkUpdates = Task.async(function* (aData, aReason = AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
function provide(obj, path, value) {
path = path.split(".");
@@ -170,17 +173,20 @@ add_task(function* checkUpdateToWebExt()
}
});
ok(!update.compatibilityUpdate, "have no compat update");
ok(update.updateAvailable, "have add-on update");
equal(update.addon.version, "1.0", "add-on version");
- yield promiseCompleteAllInstalls([update.updateAvailable]);
+ yield Promise.all([
+ promiseCompleteAllInstalls([update.updateAvailable]),
+ promiseWebExtensionStartup(),
+ ]);
let addon = yield promiseAddonByID(update.addon.id);
equal(addon.version, "1.2", "new add-on version");
addon.uninstall();
});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
@@ -27,17 +27,20 @@ add_task(function* test_implicit_id() {
// This test needs to read the xpi certificate which only works
// if signing is enabled.
ok(ADDON_SIGNING, "Add-on signing is enabled");
let addon = yield promiseAddonByID(IMPLICIT_ID_ID);
equal(addon, null, "Add-on is not installed");
let xpifile = do_get_file(IMPLICIT_ID_XPI);
- yield promiseInstallAllFiles([xpifile]);
+ yield Promise.all([
+ promiseInstallAllFiles([xpifile]),
+ promiseWebExtensionStartup(),
+ ]);
addon = yield promiseAddonByID(IMPLICIT_ID_ID);
notEqual(addon, null, "Add-on is installed");
addon.uninstall();
});
// We should also be able to install webext-implicit-id.xpi temporarily
@@ -47,17 +50,20 @@ add_task(function* test_implicit_id_temp
// This test needs to read the xpi certificate which only works
// if signing is enabled.
ok(ADDON_SIGNING, "Add-on signing is enabled");
let addon = yield promiseAddonByID(IMPLICIT_ID_ID);
equal(addon, null, "Add-on is not installed");
let xpifile = do_get_file(IMPLICIT_ID_XPI);
- yield AddonManager.installTemporaryAddon(xpifile);
+ yield Promise.all([
+ AddonManager.installTemporaryAddon(xpifile),
+ promiseWebExtensionStartup(),
+ ]);
addon = yield promiseAddonByID(IMPLICIT_ID_ID);
notEqual(addon, null, "Add-on is installed");
// The sourceURI of a temporary installed addon should be equal to the
// file url of the installed xpi file.
equal(addon.sourceURI && addon.sourceURI.spec,
Services.io.newFileURI(xpifile).spec,
@@ -75,29 +81,36 @@ add_task(function* test_unsigned_no_id_t
description: "extension without an ID",
manifest_version: 2,
version: "1.0"
};
const addonDir = yield promiseWriteWebManifestForExtension(manifest, gTmpD,
"the-addon-sub-dir");
const testDate = new Date();
- const addon = yield AddonManager.installTemporaryAddon(addonDir);
+ const [addon] = yield Promise.all([
+ AddonManager.installTemporaryAddon(addonDir),
+ promiseWebExtensionStartup(),
+ ]);
+
ok(addon.id, "ID should have been auto-generated");
ok(Math.abs(addon.installDate - testDate) < 10000, "addon has an expected installDate");
ok(Math.abs(addon.updateDate - testDate) < 10000, "addon has an expected updateDate");
// The sourceURI of a temporary installed addon should be equal to the
// file url of the installed source dir.
equal(addon.sourceURI && addon.sourceURI.spec,
Services.io.newFileURI(addonDir).spec,
"SourceURI of the add-on has the expected value");
// Install the same directory again, as if re-installing or reloading.
- const secondAddon = yield AddonManager.installTemporaryAddon(addonDir);
+ const [secondAddon] = yield Promise.all([
+ AddonManager.installTemporaryAddon(addonDir),
+ promiseWebExtensionStartup(),
+ ]);
// The IDs should be the same.
equal(secondAddon.id, addon.id, "Reinstalled add-on has the expected ID");
secondAddon.uninstall();
Services.obs.notifyObservers(addonDir, "flush-cache-entry", null);
addonDir.remove(true);
AddonTestUtils.useRealCertChecks = false;
});
@@ -475,17 +488,17 @@ add_task(function* test_strict_min_max()
notEqual(addon, null, "Add-on is installed");
equal(addon.id, newId, "Installed add-on has the expected ID");
yield extension.unload();
AddonManager.checkCompatibility = savedCheckCompatibilityValue;
});
// Check permissions prompt
-add_task(function* test_permissions() {
+add_task(function* test_permissions_prompt() {
const manifest = {
name: "permissions test",
description: "permissions test",
manifest_version: 2,
version: "1.0",
permissions: ["tabs", "storage", "https://*.example.com/*", "<all_urls>", "experiments.test"],
};
@@ -513,17 +526,17 @@ add_task(function* test_permissions() {
let addon = yield promiseAddonByID(perminfo.addon.id);
notEqual(addon, null, "Extension was installed");
addon.uninstall();
yield OS.File.remove(xpi.path);
});
// Check permissions prompt cancellation
-add_task(function* test_permissions() {
+add_task(function* test_permissions_prompt_cancel() {
const manifest = {
name: "permissions test",
description: "permissions test",
manifest_version: 2,
version: "1.0",
permissions: ["webRequestBlocking"],
};