Bug 1464743: Restore extension child shutdown timeout. r=aswan
authorKris Maglione <maglione.k@gmail.com>
Wed, 06 Jun 2018 12:43:26 -0700
changeset 476047 705c8b40f4c1c51bd4f9c46a5d854ec439454ff2
parent 476046 3f9536c4da4d8a22545b83eebca4d1d62b236535
child 476048 5cd3f1a923eff54b648a02a24a52c37c17b4412b
push id9374
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:43:20 +0000
treeherdermozilla-beta@160e085dfb0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1464743
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1464743: Restore extension child shutdown timeout. r=aswan MozReview-Commit-ID: 8O6CgKsOwom
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionUtils.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -93,29 +93,33 @@ var {
   ParentAPIManager,
   StartupCache,
   apiManager: Management,
 } = ExtensionParent;
 
 const {
   EventEmitter,
   getUniqueId,
+  promiseTimeout,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
 
 XPCOMUtils.defineLazyGetter(this, "LocaleData", () => ExtensionCommon.LocaleData);
 
 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
 // storage used by the browser.storage.local API is not directly accessible from the extension code).
 XPCOMUtils.defineLazyGetter(this, "WEBEXT_STORAGE_USER_CONTEXT_ID", () => {
   return ContextualIdentityService.getDefaultPrivateIdentity(
     "userContextIdInternal.webextStorageLocal").userContextId;
 });
 
+// The maximum time to wait for extension child shutdown blockers to complete.
+const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;
+
 /**
  * Classify an individual permission from a webextension manifest
  * as a host/origin permission, an api permission, or a regular permission.
  *
  * @param {string} perm  The permission string to classify
  *
  * @returns {object}
  *          An object with exactly one of the following properties:
@@ -1810,17 +1814,25 @@ class Extension extends ExtensionData {
       obj.close();
     }
 
     ParentAPIManager.shutdownExtension(this.id);
 
     Management.emit("shutdown", this);
     this.emit("shutdown");
 
-    await this.broadcast("Extension:Shutdown", {id: this.id});
+    const TIMED_OUT = Symbol();
+
+    let result = await Promise.race([
+      this.broadcast("Extension:Shutdown", {id: this.id}),
+      promiseTimeout(CHILD_SHUTDOWN_TIMEOUT_MS).then(() => TIMED_OUT),
+    ]);
+    if (result === TIMED_OUT) {
+      Cu.reportError(`Timeout while waiting for extension child to shutdown: ${this.policy.debugName}`);
+    }
 
     MessageChannel.abortResponses({extensionId: this.id});
 
     this.policy.active = false;
 
     return this.cleanupGeneratedFile();
   }
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -7,16 +7,18 @@
 
 var EXPORTED_SYMBOLS = ["ExtensionUtils"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "ConsoleAPI",
                                "resource://gre/modules/Console.jsm");
+ChromeUtils.defineModuleGetter(this, "setTimeout",
+                               "resource://gre/modules/Timer.jsm");
 
 function getConsole() {
   return new ConsoleAPI({
     maxLogLevelPref: "extensions.webextensions.log.level",
     prefix: "WebExtensions",
   });
 }
 
@@ -41,16 +43,19 @@ const uniqueProcessID = appinfo.uniquePr
 const processIDMask = (uniqueProcessID & 0xffff) * (2 ** 37);
 
 function getUniqueId() {
   // Note: We can't use bitwise ops here, since they truncate to a 32 bit
   // integer and we need all 53 mantissa bits.
   return processIDMask + nextId++;
 }
 
+function promiseTimeout(delay) {
+  return new Promise(resolve => setTimeout(resolve, delay));
+}
 
 /**
  * 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) {
@@ -684,16 +689,17 @@ var ExtensionUtils = {
   instanceOf,
   makeWidgetId,
   normalizeTime,
   promiseDocumentIdle,
   promiseDocumentLoaded,
   promiseDocumentReady,
   promiseEvent,
   promiseObserved,
+  promiseTimeout,
   runSafeSyncWithoutClone,
   withHandlingUserInput,
   DefaultMap,
   DefaultWeakMap,
   EventEmitter,
   ExtensionError,
   LimitedSet,
   MessageManagerProxy,