Bug 1329021 - Stop an embedded webextension when a shutdown bootstrap method is not defined. r?aswan draft
authorLuca Greco <lgreco@mozilla.com>
Fri, 06 Jan 2017 04:11:12 -0500
changeset 458537 9ea48c2dfbe727f6f0e5e7a5530f89d3ae141994
parent 458532 7011ed1427de2b6f075c46cc6f4618d3e9fcd2a4
child 541658 58cf5f9582b8fbc35d71ee71c38d906ac37bc7ce
push id40966
push userluca.greco@alcacoop.it
push dateTue, 10 Jan 2017 12:15:05 +0000
reviewersaswan
bugs1329021
milestone53.0a1
Bug 1329021 - Stop an embedded webextension when a shutdown bootstrap method is not defined. r?aswan MozReview-Commit-ID: 4VTDRpadaHO
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -4856,21 +4856,16 @@ this.XPIProvider = {
       try {
         method = Components.utils.evalInSandbox(`${aMethod};`,
           activeAddon.bootstrapScope, "ECMAv5");
       } catch (e) {
         // An exception will be caught if the expected method is not defined.
         // That will be logged below.
       }
 
-      if (!method) {
-        logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod);
-        return;
-      }
-
       // Extensions are automatically deinitialized in the correct order at shutdown.
       if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
         activeAddon.disable = true;
         for (let addon of this.getDependentAddons(aAddon)) {
           if (addon.active)
             this.updateAddonDisabledState(addon);
         }
       }
@@ -4894,22 +4889,26 @@ this.XPIProvider = {
           params.webExtension = {
             startup: () => webExtension.startup(),
           };
         } else if (aMethod == "shutdown") {
           LegacyExtensionsUtils.getEmbeddedExtensionFor(params).shutdown();
         }
       }
 
-      logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " +
-                   aAddon.version);
-      try {
-        method(params, aReason);
-      } catch (e) {
-        logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e);
+      if (!method) {
+        logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod);
+      } else {
+        logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " +
+                     aAddon.version);
+        try {
+          method(params, aReason);
+        } catch (e) {
+          logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e);
+        }
       }
     } finally {
       // Extensions are automatically initialized in the correct order at startup.
       if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) {
         for (let addon of this.getDependentAddons(aAddon))
           this.updateAddonDisabledState(addon);
       }
 
--- a/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
@@ -18,13 +18,13 @@ function notify(event, originalMethod, d
 
   // If the bootstrap scope already declares a method call it
   if (originalMethod)
     originalMethod(data, reason);
 }
 
 // Allows a simple one-line bootstrap script:
 // Components.utils.import("resource://xpcshelldata/bootstrapmonitor.jsm").monitor(this);
-this.monitor = function(scope) {
-  for (let event of ["install", "startup", "shutdown", "uninstall"]) {
+this.monitor = function(scope, methods = ["install", "startup", "shutdown", "uninstall"]) {
+  for (let event of methods) {
     scope[event] = notify.bind(null, event, scope[event]);
   }
 }
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
@@ -42,16 +42,22 @@ function promiseWebExtensionShutdown() {
     Management.on("shutdown", listener);
   });
 }
 
 const BOOTSTRAP = String.raw`
   Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
 `;
 
+const BOOTSTRAP_WITHOUT_SHUTDOWN = String.raw`
+  Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this, [
+    "install", "startup", "uninstall",
+  ]);
+`;
+
 const EMBEDDED_WEBEXT_MANIFEST = JSON.stringify({
   name: "embedded webextension addon",
   manifest_version: 2,
   version: "1.0",
 });
 
 /**
  *  This test case checks that an hasEmbeddedWebExtension addon property
@@ -298,8 +304,80 @@ add_task(function* reload_embedded_webex
   let waitUninstalled = promiseAddonEvent("onUninstalled");
   addon.uninstall();
   yield waitUninstalled;
 
   // No leaked embedded extension after uninstalling.
   equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
         "No embedded extension instance should be tracked after the addon uninstall");
 });
+
+/**
+ *  This test case checks that an addon with hasEmbeddedWebExtension without
+ *  a bootstrap shutdown method stops the embedded webextension.
+ */
+add_task(function* shutdown_embedded_webext_without_bootstrap_shutdown() {
+  const ID = "embedded-webextension-without-shutdown@tests.mozilla.org";
+
+  // No embedded webextension should be currently around.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+        "No embedded extension instance should be tracked here");
+
+  const xpiFile = createTempXPIFile({
+    id: ID,
+    name: "Test Add-on",
+    version: "1.0",
+    bootstrap: true,
+    hasEmbeddedWebExtension: true,
+    targetApplications: [{
+      id: "xpcshell@tests.mozilla.org",
+      minVersion: "1",
+      maxVersion: "1.9.2"
+    }]
+  }, {
+    "bootstrap.js": BOOTSTRAP_WITHOUT_SHUTDOWN,
+    "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+  });
+
+  yield AddonManager.installTemporaryAddon(xpiFile);
+
+  let addon = yield promiseAddonByID(ID);
+
+  notEqual(addon, null, "Got an addon object as expected");
+  equal(addon.version, "1.0", "Got the expected version");
+  equal(addon.isActive, true, "The Addon is active");
+  equal(addon.appDisabled, false, "The addon is not app disabled");
+  equal(addon.userDisabled, false, "The addon is not user disabled");
+
+  // Check that the addon has been installed and started.
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+  // Only one embedded extension.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+        "Got the expected number of tracked extension instances");
+
+  const startupInfo = BootstrapMonitor.started.get(ID);
+
+  const waitForWebExtensionStartup = promiseWebExtensionStartup();
+
+  yield startupInfo.data.webExtension.startup();
+
+  // WebExtension startup should have been fully resolved.
+  yield waitForWebExtensionStartup;
+
+  // Fake the BootstrapMonitor notification, or the shutdown checks defined in head_addons.js
+  // will fail because of the missing shutdown method.
+  const fakeShutdownInfo = Object.assign(startupInfo, {
+    event: "shutdown",
+    reason: 2,
+  });
+  Services.obs.notifyObservers({}, "bootstrapmonitor-event", JSON.stringify(fakeShutdownInfo));
+
+  // Uninstall the addon.
+  const waitUninstalled = promiseAddonEvent("onUninstalled");
+  addon.uninstall();
+  yield waitUninstalled;
+
+  // No leaked embedded extension after uninstalling.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+        "No embedded extension instance should be tracked after the addon uninstall");
+});