summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/parent/ext-runtime.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/parent/ext-runtime.js')
-rw-r--r--toolkit/components/extensions/parent/ext-runtime.js310
1 files changed, 310 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/ext-runtime.js b/toolkit/components/extensions/parent/ext-runtime.js
new file mode 100644
index 0000000000..f4f9ea6616
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-runtime.js
@@ -0,0 +1,310 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This file expects tabTracker to be defined in the global scope (e.g.
+// by ext-browser.js or ext-android.js).
+/* global tabTracker */
+
+var { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
+ DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gRuntimeTimeout",
+ "extensions.webextensions.runtime.timeout",
+ 5000
+);
+
+this.runtime = class extends ExtensionAPIPersistent {
+ PERSISTENT_EVENTS = {
+ // Despite not being part of PERSISTENT_EVENTS, the following events are
+ // still triggered (after waking up the background context if needed):
+ // - runtime.onConnect
+ // - runtime.onConnectExternal
+ // - runtime.onMessage
+ // - runtime.onMessageExternal
+ // For details, see bug 1852317 and test_ext_eventpage_messaging_wakeup.js.
+
+ onInstalled({ fire }) {
+ let { extension } = this;
+ let temporary = !!extension.addonData.temporarilyInstalled;
+
+ let listener = () => {
+ switch (extension.startupReason) {
+ case "APP_STARTUP":
+ if (AddonManagerPrivate.browserUpdated) {
+ fire.sync({ reason: "browser_update", temporary });
+ }
+ break;
+ case "ADDON_INSTALL":
+ fire.sync({ reason: "install", temporary });
+ break;
+ case "ADDON_UPGRADE":
+ fire.sync({
+ reason: "update",
+ previousVersion: extension.addonData.oldVersion,
+ temporary,
+ });
+ break;
+ }
+ };
+ extension.on("background-first-run", listener);
+ return {
+ unregister() {
+ extension.off("background-first-run", listener);
+ },
+ convert(_fire) {
+ fire = _fire;
+ },
+ };
+ },
+ onUpdateAvailable({ fire }) {
+ let { extension } = this;
+ let instanceID = extension.addonData.instanceID;
+ AddonManager.addUpgradeListener(instanceID, upgrade => {
+ extension.upgrade = upgrade;
+ let details = {
+ version: upgrade.version,
+ };
+ fire.sync(details);
+ });
+ return {
+ unregister() {
+ AddonManager.removeUpgradeListener(instanceID);
+ },
+ convert(_fire) {
+ fire = _fire;
+ },
+ };
+ },
+ onPerformanceWarning({ fire }) {
+ let { extension } = this;
+
+ let observer = (subject, topic) => {
+ let report = subject.QueryInterface(Ci.nsIHangReport);
+
+ if (report?.addonId !== extension.id) {
+ return;
+ }
+
+ const performanceWarningEventDetails = {
+ category: "content_script",
+ severity: "high",
+ description:
+ "Slow extension content script caused a page hang, user was warned.",
+ };
+
+ let scriptBrowser = report.scriptBrowser;
+ let nativeTab =
+ scriptBrowser?.ownerGlobal.gBrowser?.getTabForBrowser(scriptBrowser);
+ if (nativeTab) {
+ performanceWarningEventDetails.tabId = tabTracker.getId(nativeTab);
+ }
+
+ fire.async(performanceWarningEventDetails);
+ };
+
+ Services.obs.addObserver(observer, "process-hang-report");
+ return {
+ unregister: () => {
+ Services.obs.removeObserver(observer, "process-hang-report");
+ },
+ convert(_fire, context) {
+ fire = _fire;
+ },
+ };
+ },
+ };
+
+ getAPI(context) {
+ let { extension } = context;
+ return {
+ runtime: {
+ // onStartup is special-cased in ext-backgroundPages to cause
+ // an immediate startup. We do not prime onStartup.
+ onStartup: new EventManager({
+ context,
+ module: "runtime",
+ event: "onStartup",
+ register: fire => {
+ if (context.incognito || extension.startupReason != "APP_STARTUP") {
+ // This event should not fire if we are operating in a private profile.
+ return () => {};
+ }
+ let listener = () => {
+ return fire.sync();
+ };
+
+ extension.on("background-first-run", listener);
+
+ return () => {
+ extension.off("background-first-run", listener);
+ };
+ },
+ }).api(),
+
+ onInstalled: new EventManager({
+ context,
+ module: "runtime",
+ event: "onInstalled",
+ extensionApi: this,
+ }).api(),
+
+ onUpdateAvailable: new EventManager({
+ context,
+ module: "runtime",
+ event: "onUpdateAvailable",
+ extensionApi: this,
+ }).api(),
+
+ onSuspend: new EventManager({
+ context,
+ name: "runtime.onSuspend",
+ resetIdleOnEvent: false,
+ register: fire => {
+ let listener = async () => {
+ let timedOut = false;
+ async function promiseFire() {
+ try {
+ await fire.async();
+ } catch (e) {}
+ }
+ await Promise.race([
+ promiseFire(),
+ ExtensionUtils.promiseTimeout(gRuntimeTimeout).then(() => {
+ timedOut = true;
+ }),
+ ]);
+ if (timedOut) {
+ Cu.reportError(
+ `runtime.onSuspend in ${extension.id} took too long`
+ );
+ }
+ };
+ extension.on("background-script-suspend", listener);
+ return () => {
+ extension.off("background-script-suspend", listener);
+ };
+ },
+ }).api(),
+
+ onSuspendCanceled: new EventManager({
+ context,
+ name: "runtime.onSuspendCanceled",
+ register: fire => {
+ let listener = () => {
+ fire.async();
+ };
+ extension.on("background-script-suspend-canceled", listener);
+ return () => {
+ extension.off("background-script-suspend-canceled", listener);
+ };
+ },
+ }).api(),
+
+ onPerformanceWarning: new EventManager({
+ context,
+ module: "runtime",
+ event: "onPerformanceWarning",
+ extensionApi: this,
+ }).api(),
+
+ reload: async () => {
+ if (extension.upgrade) {
+ // If there is a pending update, install it now.
+ extension.upgrade.install();
+ } else {
+ // Otherwise, reload the current extension.
+ let addon = await AddonManager.getAddonByID(extension.id);
+ addon.reload();
+ }
+ },
+
+ get lastError() {
+ // TODO(robwu): Figure out how to make sure that errors in the parent
+ // process are propagated to the child process.
+ // lastError should not be accessed from the parent.
+ return context.lastError;
+ },
+
+ getBrowserInfo: function () {
+ const { name, vendor, version, appBuildID } = Services.appinfo;
+ const info = { name, vendor, version, buildID: appBuildID };
+ return Promise.resolve(info);
+ },
+
+ getPlatformInfo: function () {
+ return Promise.resolve(ExtensionParent.PlatformInfo);
+ },
+
+ openOptionsPage: function () {
+ if (!extension.manifest.options_ui) {
+ return Promise.reject({ message: "No `options_ui` declared" });
+ }
+
+ // This expects openOptionsPage to be defined in the file using this,
+ // e.g. the browser/ version of ext-runtime.js
+ /* global openOptionsPage:false */
+ return openOptionsPage(extension).then(() => {});
+ },
+
+ setUninstallURL: function (url) {
+ if (url === null || url.length === 0) {
+ extension.uninstallURL = null;
+ return Promise.resolve();
+ }
+
+ let uri;
+ try {
+ uri = new URL(url);
+ } catch (e) {
+ return Promise.reject({
+ message: `Invalid URL: ${JSON.stringify(url)}`,
+ });
+ }
+
+ if (uri.protocol != "http:" && uri.protocol != "https:") {
+ return Promise.reject({
+ message: "url must have the scheme http or https",
+ });
+ }
+
+ extension.uninstallURL = url;
+ return Promise.resolve();
+ },
+
+ // This function is not exposed to the extension js code and it is only
+ // used by the alert function redefined into the background pages to be
+ // able to open the BrowserConsole from the main process.
+ openBrowserConsole() {
+ if (AppConstants.platform !== "android") {
+ DevToolsShim.openBrowserConsole();
+ }
+ },
+
+ async internalWakeupBackground() {
+ const { background } = extension.manifest;
+ if (
+ background &&
+ (background.page || background.scripts) &&
+ // Note: if background.service_worker is specified, it takes
+ // precedence over page/scripts, and persistentBackground is false.
+ !extension.persistentBackground
+ ) {
+ await extension.wakeupBackground();
+ }
+ },
+ },
+ };
+ }
+};