Bug 1059674 - use AsynchShutdown.blocker() for AddonManager provider shutdown; r=unfocused
authorIrving Reid <irving@mozilla.com>
Sat, 27 Sep 2014 16:25:47 -0400
changeset 207541 8ae4bfe77e2197e378497990c28cc06c85c22878
parent 207540 df78a0e806d73dcc2f5a8253c6da763894452b6e
child 207542 7349529e5a8134af7a85d57e1cf0df94c81d00db
push id27561
push userttaubert@mozilla.com
push dateMon, 29 Sep 2014 09:17:22 +0000
treeherdermozilla-central@9d66436af432 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersunfocused
bugs1059674
milestone35.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 1059674 - use AsynchShutdown.blocker() for AddonManager provider shutdown; r=unfocused
browser/experiments/Experiments.jsm
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/LightweightThemeManager.jsm
toolkit/mozapps/extensions/internal/OpenH264Provider.jsm
toolkit/mozapps/extensions/internal/PluginProvider.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -2255,16 +2255,18 @@ this.Experiments.PreviousExperimentProvi
   this._experiments = experiments;
   this._experimentList = [];
   this._log = Log.repository.getLoggerWithMessagePrefix(
     "Browser.Experiments.Experiments",
     "PreviousExperimentProvider #" + gPreviousProviderCounter++ + "::");
 }
 
 this.Experiments.PreviousExperimentProvider.prototype = Object.freeze({
+  get name() "PreviousExperimentProvider",
+
   startup: function () {
     this._log.trace("startup()");
     Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
   },
 
   shutdown: function () {
     this._log.trace("shutdown()");
     try {
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -117,16 +117,18 @@ let logger = Log.repository.getLogger(LO
 // If the "extensions.logging.enabled" preference is
 // missing or 'false', messages at the WARNING and higher
 // severity should be logged to the JS console and standard error.
 // If "extensions.logging.enabled" is set to 'true', messages
 // at DEBUG and higher should go to JS console and standard error.
 const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
 
+const UNNAMED_PROVIDER = "<unnamed-provider>";
+
 /**
  * Preference listener which listens for a change in the
  * "extensions.logging.enabled" preference and changes the logging level of the
  * parent 'addons' level logger accordingly.
  */
 var PrefObserver = {
     init: function PrefObserver_init() {
       Services.prefs.addObserver(PREF_LOGGING_ENABLED, this, false);
@@ -459,27 +461,30 @@ var gStartupComplete = false;
 var gCheckCompatibility = true;
 var gStrictCompatibility = true;
 var gCheckUpdateSecurityDefault = true;
 var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
 var gUpdateEnabled = true;
 var gAutoUpdateDefault = true;
 var gHotfixID = null;
 var gShutdownBarrier = null;
+var gRepoShutdownState = "";
+var gShutdownInProgress = false;
 
 /**
  * This is the real manager, kept here rather than in AddonManager to keep its
  * contents hidden from API users.
  */
 var AddonManagerInternal = {
   managerListeners: [],
   installListeners: [],
   addonListeners: [],
   typeListeners: [],
   providers: [],
+  providerShutdowns: new Map(),
   types: {},
   startupChanges: {},
   // Store telemetry details per addon provider
   telemetryDetails: {},
 
   // A read-only wrapper around the types dictionary
   typesProxy: Proxy.create({
     getOwnPropertyDescriptor: function typesProxy_getOwnPropertyDescriptor(aName) {
@@ -609,16 +614,43 @@ var AddonManagerInternal = {
       appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST);
     }
     catch (e) {
       logger.warn("Failed to copy the application shipped blocklist to the profile", e);
     }
   },
 
   /**
+   * Start up a provider, and register its shutdown hook if it has one
+   */
+  _startProvider(aProvider, aAppChanged, aOldAppVersion, aOldPlatformVersion) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
+    callProvider(aProvider, "startup", null, aAppChanged, aOldAppVersion, aOldPlatformVersion);
+    if ('shutdown' in aProvider) {
+      let name = aProvider.name || "Provider";
+      let AMProviderShutdown = () => {
+        return new Promise((resolve, reject) => {
+            logger.debug("Calling shutdown blocker for " + name);
+            resolve(aProvider.shutdown());
+          })
+          .catch(err => {
+            logger.warn("Failure during shutdown of " + name, err);
+            AddonManagerPrivate.recordException("AMI", "Async shutdown of " + name, err);
+          });
+      };
+      logger.debug("Registering shutdown blocker for " + name);
+      this.providerShutdowns.set(aProvider, AMProviderShutdown);
+      AddonManager.shutdown.addBlocker(name, AMProviderShutdown);
+    }
+  },
+
+  /**
    * Initializes the AddonManager, loading any known providers and initializing
    * them.
    */
   startup: function AMI_startup() {
     try {
       if (gStarted)
         return;
 
@@ -737,25 +769,27 @@ var AddonManagerInternal = {
         catch (e) {
           AddonManagerPrivate.recordException("AMI", "provider " + url + " load failed", e);
           logger.error("Exception loading provider " + entry + " from category \"" +
                 url + "\"", e);
         }
       }
 
       // Register our shutdown handler with the AsyncShutdown manager
-      gShutdownBarrier = new AsyncShutdown.Barrier("AddonManager: Waiting for clients to shut down.");
-      AsyncShutdown.profileBeforeChange.addBlocker("AddonManager: shutting down providers",
-                                                   this.shutdownManager.bind(this));
+      gShutdownBarrier = new AsyncShutdown.Barrier("AddonManager: Waiting for providers to shut down.");
+      AsyncShutdown.profileBeforeChange.addBlocker("AddonManager: shutting down.",
+                                                   this.shutdownManager.bind(this),
+                                                   {fetchState: this.shutdownState.bind(this)});
 
       // Once we start calling providers we must allow all normal methods to work.
       gStarted = true;
 
-      this.callProviders("startup", appChanged, oldAppVersion,
-                         oldPlatformVersion);
+      for (let provider of this.providers) {
+        this._startProvider(provider, appChanged, oldAppVersion, oldPlatformVersion);
+      }
 
       // If this is a new profile just pretend that there were no changes
       if (appChanged === undefined) {
         for (let type in this.startupChanges)
           delete this.startupChanges[type];
       }
 
       gStartupComplete = true;
@@ -808,25 +842,29 @@ var AddonManagerInternal = {
         }
         else {
           this.types[aType.id].providers.push(aProvider);
         }
       }, this);
     }
 
     // If we're registering after startup call this provider's startup.
-    if (gStarted)
-      callProvider(aProvider, "startup");
+    if (gStarted) {
+      this._startProvider(aProvider);
+    }
   },
 
   /**
    * Unregisters an AddonProvider.
    *
    * @param  aProvider
    *         The provider to unregister
+   * @return Whatever the provider's 'shutdown' method returns (if anything).
+   *         For providers that have async shutdown methods returning Promises,
+   *         the caller should wait for that Promise to resolve.
    */
   unregisterProvider: function AMI_unregisterProvider(aProvider) {
     if (!aProvider || typeof aProvider != "object")
       throw Components.Exception("aProvider must be specified",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     let pos = 0;
     while (pos < this.providers.length) {
@@ -846,19 +884,29 @@ var AddonManagerInternal = {
         for (let listener of typeListeners) {
           safeCall(function listenerSafeCall() {
             listener.onTypeRemoved(oldType);
           });
         }
       }
     }
 
-    // If we're unregistering after startup call this provider's shutdown.
-    if (gStarted)
-      callProvider(aProvider, "shutdown");
+    // If we're unregistering after startup but before shutting down,
+    // remove the blocker for this provider's shutdown and call it.
+    // If we're already shutting down, just let gShutdownBarrier call it to avoid races.
+    if (gStarted && !gShutdownInProgress) {
+      logger.debug("Unregistering shutdown blocker for " + (aProvider.name || "Provider"));
+      let shutter = this.providerShutdowns.get(aProvider);
+      if (shutter) {
+        this.providerShutdowns.delete(aProvider);
+        gShutdownBarrier.client.removeBlocker(shutter);
+        return shutter();
+      }
+    }
+    return undefined;
   },
 
   /**
    * Calls a method on all registered providers if it exists and consumes any
    * thrown exception. Return values are ignored. Any parameters after the
    * method parameter are passed to the provider's method.
    *
    * @param  aMethod
@@ -879,105 +927,92 @@ var AddonManagerInternal = {
       catch (e) {
         AddonManagerPrivate.recordException("AMI", "provider " + aMethod, e);
         logger.error("Exception calling provider " + aMethod, e);
       }
     }
   },
 
   /**
-   * Calls a method on all registered providers, if the provider implements
-   * the method. The called method is expected to return a promise, and
-   * callProvidersAsync returns a promise that resolves when every provider
-   * method has either resolved or rejected. Rejection reasons are logged
-   * but otherwise ignored. Return values are ignored. Any parameters after the
-   * method parameter are passed to the provider's method.
-   *
-   * @param  aMethod
-   *         The method name to call
-   * @see    callProvider
+   * Report the current state of asynchronous shutdown
    */
-  callProvidersAsync: function AMI_callProviders(aMethod, ...aArgs) {
-    if (!aMethod || typeof aMethod != "string")
-      throw Components.Exception("aMethod must be a non-empty string",
-                                 Cr.NS_ERROR_INVALID_ARG);
-
-    let allProviders = [];
-
-    let providers = this.providers.slice(0);
-    for (let provider of providers) {
-      try {
-        if (aMethod in provider) {
-          // Resolve a new promise with the result of the method, to handle both
-          // methods that return values (or nothing) and methods that return promises.
-          let providerResult = provider[aMethod].apply(provider, aArgs);
-          let nextPromise = Promise.resolve(providerResult);
-          // Log and swallow the errors from methods that do return promises.
-          nextPromise = nextPromise.then(
-              null,
-              e => logger.error("Exception calling provider " + aMethod, e));
-          allProviders.push(nextPromise);
-        }
-      }
-      catch (e) {
-        logger.error("Exception calling provider " + aMethod, e);
-      }
+  shutdownState() {
+    let state = [];
+    if (gShutdownBarrier) {
+      state.push({
+        name: gShutdownBarrier.client.name,
+        state: gShutdownBarrier.state
+      });
     }
-    // Because we use promise.then to catch and log all errors above, Promise.all()
-    // will never exit early because of a rejection.
-    return Promise.all(allProviders);
+    state.push({
+      name: "AddonRepository: async shutdown",
+      state: gRepoShutdownState
+    });
+    return state;
   },
 
   /**
    * Shuts down the addon manager and all registered providers, this must clean
    * up everything in order for automated tests to fake restarts.
    * @return Promise{null} that resolves when all providers and dependent modules
    *                       have finished shutting down
    */
-  shutdownManager: function() {
+  shutdownManager: Task.async(function* () {
     logger.debug("shutdown");
+    gRepoShutdownState = "pending";
+    gShutdownInProgress = true;
     // Clean up listeners
     Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this);
     Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this);
     Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
     Services.prefs.removeObserver(PREF_EM_UPDATE_ENABLED, this);
     Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
     Services.prefs.removeObserver(PREF_EM_HOTFIX_ID, this);
 
-    // Only shut down providers if they've been started. Shut down
-    // AddonRepository after providers (if any).
-    let shuttingDown = null;
+    let savedError = null;
+    // Only shut down providers if they've been started.
     if (gStarted) {
-      shuttingDown = gShutdownBarrier.wait()
-        .then(null, err => logger.error("Failure during wait for shutdown barrier", err))
-        .then(() => this.callProvidersAsync("shutdown"))
-        .then(null,
-              err => logger.error("Failure during async provider shutdown", err))
-        .then(() => AddonRepository.shutdown());
+      try {
+        yield gShutdownBarrier.wait();
+      }
+      catch(err) {
+        savedError = err;
+        logger.error("Failure during wait for shutdown barrier", err);
+        AddonManagerPrivate.recordException("AMI", "Async shutdown of AddonRepository", err);
+      }
     }
-    else {
-      shuttingDown = AddonRepository.shutdown();
+
+    // Shut down AddonRepository after providers (if any).
+    try {
+      gRepoShutdownState = "in progress";
+      yield AddonRepository.shutdown();
+      gRepoShutdownState = "done";
     }
-
-    shuttingDown.then(val => logger.debug("Async provider shutdown done"),
-                      err => logger.error("Failure during AddonRepository shutdown", err))
-      .then(() => {
-        this.managerListeners.splice(0, this.managerListeners.length);
-        this.installListeners.splice(0, this.installListeners.length);
-        this.addonListeners.splice(0, this.addonListeners.length);
-        this.typeListeners.splice(0, this.typeListeners.length);
-        for (let type in this.startupChanges)
-          delete this.startupChanges[type];
-        gStarted = false;
-        gStartupComplete = false;
-        gShutdownBarrier = null;
-      });
-
-    return shuttingDown;
-  },
+    catch(err) {
+      savedError = err;
+      logger.error("Failure during AddonRepository shutdown", err);
+      AddonManagerPrivate.recordException("AMI", "Async shutdown of AddonRepository", err);
+    }
+
+    logger.debug("Async provider shutdown done");
+    this.managerListeners.splice(0, this.managerListeners.length);
+    this.installListeners.splice(0, this.installListeners.length);
+    this.addonListeners.splice(0, this.addonListeners.length);
+    this.typeListeners.splice(0, this.typeListeners.length);
+    this.providerShutdowns.clear();
+    for (let type in this.startupChanges)
+      delete this.startupChanges[type];
+    gStarted = false;
+    gStartupComplete = false;
+    gShutdownBarrier = null;
+    gShutdownInProgress = false;
+    if (savedError) {
+      throw savedError;
+    }
+  }),
 
   /**
    * Notified when a preference we're interested in has changed.
    *
    * @see nsIObserver
    */
   observe: function AMI_observe(aSubject, aTopic, aData) {
     switch (aData) {
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -65,16 +65,18 @@ this.__defineSetter__("_maxUsedThemes", 
 
 // Holds the ID of the theme being enabled or disabled while sending out the
 // events so cached AddonWrapper instances can return correct values for
 // permissions and pendingOperations
 var _themeIDBeingEnabled = null;
 var _themeIDBeingDisabled = null;
 
 this.LightweightThemeManager = {
+  get name() "LightweightThemeManager",
+
   get usedThemes () {
     try {
       return JSON.parse(_prefs.getComplexValue("usedThemes",
                                                Ci.nsISupportsString).data);
     } catch (e) {
       return [];
     }
   },
--- a/toolkit/mozapps/extensions/internal/OpenH264Provider.jsm
+++ b/toolkit/mozapps/extensions/internal/OpenH264Provider.jsm
@@ -240,16 +240,18 @@ let OpenH264Wrapper = {
   },
 
   get isInstalled() {
     return this.version.length > 0;
   },
 };
 
 let OpenH264Provider = {
+  get name() "OpenH264Provider",
+
   startup: function() {
     configureLogging();
     this._log = Log.repository.getLoggerWithMessagePrefix("Toolkit.OpenH264Provider",
                                                           "OpenH264Provider" + "::");
     OpenH264Wrapper._log = Log.repository.getLoggerWithMessagePrefix("Toolkit.OpenH264Provider",
                                                                      "OpenH264Wrapper" + "::");
     this.gmpPath = null;
     if (OpenH264Wrapper.isInstalled) {
--- a/toolkit/mozapps/extensions/internal/PluginProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
@@ -43,16 +43,18 @@ function getIDHashForString(aStr) {
   return "{" + hash.substr(0, 8) + "-" +
                hash.substr(8, 4) + "-" +
                hash.substr(12, 4) + "-" +
                hash.substr(16, 4) + "-" +
                hash.substr(20) + "}";
 }
 
 var PluginProvider = {
+  get name() "PluginProvider",
+
   // A dictionary mapping IDs to names and descriptions
   plugins: null,
 
   startup: function PL_startup() {
     Services.obs.addObserver(this, LIST_UPDATED_TOPIC, false);
     Services.obs.addObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false);
   },
 
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -1782,16 +1782,18 @@ this.XPIStates = {
     if (location.size == 0) {
       this.db.delete(aLocation);
     }
     this.save();
   },
 };
 
 this.XPIProvider = {
+  get name() "XPIProvider",
+
   // An array of known install locations
   installLocations: null,
   // A dictionary of known install locations by name
   installLocationsByName: null,
   // An array of currently active AddonInstalls
   installs: null,
   // The default skin for the application
   defaultSkin: "classic/1.0",
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -21,52 +21,40 @@ const TIMEOUT_MS = 900000;
 Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 Components.utils.import("resource://gre/modules/Promise.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/AsyncShutdown.jsm");
 
 Services.prefs.setBoolPref("toolkit.osfile.log", true);
 
 // We need some internal bits of AddonManager
 let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
 let AddonManager = AMscope.AddonManager;
 let AddonManagerInternal = AMscope.AddonManagerInternal;
 // Mock out AddonManager's reference to the AsyncShutdown module so we can shut
 // down AddonManager from the test
 let MockAsyncShutdown = {
   hook: null,
+  status: null,
   profileBeforeChange: {
-    addBlocker: function(aName, aBlocker) {
+    addBlocker: function(aName, aBlocker, aOptions) {
       do_print("Mock profileBeforeChange blocker for '" + aName + "'");
       MockAsyncShutdown.hook = aBlocker;
+      MockAsyncShutdown.status = aOptions.fetchState;
     }
   },
-  Barrier: function (name) {
-    this.name = name;
-    this.client.addBlocker = (name, blocker) => {
-      do_print("Mock Barrier blocker for '" + name + "' for barrier '" + this.name + "'");
-      this.blockers.push({name: name, blocker: blocker});
-    };
-  },
+  // We can use the real Barrier
+  Barrier: AsyncShutdown.Barrier
 };
 
-MockAsyncShutdown.Barrier.prototype = Object.freeze({
-  blockers: [],
-  client: {},
-  wait: Task.async(function* () {
-    for (let b of this.blockers) {
-      yield b.blocker();
-    }
-  }),
-});
-
 AMscope.AsyncShutdown = MockAsyncShutdown;
 
 var gInternalManager = null;
 var gAppInfo = null;
 var gAddonsList;
 
 var gPort = null;
 var gUrlToFileMap = {};
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Verify that we report shutdown status for Addon Manager providers
+// and AddonRepository correctly.
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+// Make a mock AddonRepository that just lets us hang shutdown.
+// Needs two promises - one to let us know that AM has called shutdown,
+// and one for us to let AM know that shutdown is done.
+function mockAddonProvider(aName) {
+  let mockProvider = {
+    donePromise: null,
+    doneResolve: null,
+    doneReject: null,
+    shutdownPromise: null,
+    shutdownResolve: null,
+
+    get name() aName,
+
+    shutdown() {
+      this.shutdownResolve();
+      return this.donePromise;
+    },
+  };
+  mockProvider.donePromise = new Promise((resolve, reject) => {
+    mockProvider.doneResolve = resolve;
+    mockProvider.doneResject = reject;
+  });
+  mockProvider.shutdownPromise = new Promise((resolve, reject) => {
+    mockProvider.shutdownResolve = resolve;
+  });
+  return mockProvider;
+};
+
+function run_test() {
+  run_next_test();
+}
+
+// Helper to find a particular shutdown blocker's status in the JSON blob
+function findInStatus(aStatus, aName) {
+  for (let {name, state} of aStatus.state) {
+    if (name == aName) {
+      return state;
+    }
+  }
+  return null;
+}
+
+/*
+ * Make sure we report correctly when an add-on provider or AddonRepository block shutdown
+ */
+add_task(function* blockRepoShutdown() {
+  // Reach into the AddonManager scope and inject our mock AddonRepository
+  let realAddonRepo = AMscope.AddonRepository;
+  // the mock provider behaves enough like AddonRepository for the purpose of this test
+  let mockRepo = mockAddonProvider("Mock repo");
+  AMscope.AddonRepository = mockRepo;
+
+  let mockProvider = mockAddonProvider("Mock provider");
+
+  startupManager();
+  AddonManagerPrivate.registerProvider(mockProvider);
+
+  // Start shutting the manager down
+  let managerDown = promiseShutdownManager();
+
+  // Wait for manager to call provider shutdown.
+  yield mockProvider.shutdownPromise;
+  // check AsyncShutdown state
+  let status = MockAsyncShutdown.status();
+  equal(findInStatus(status[0], "Mock provider"), "(none)");
+  equal(status[1].name, "AddonRepository: async shutdown");
+  equal(status[1].state, "pending");
+  // let the provider finish
+  mockProvider.doneResolve();
+
+  // Wait for manager to call repo shutdown and start waiting for it
+  yield mockRepo.shutdownPromise;
+  // Check the shutdown state
+  status = MockAsyncShutdown.status();
+  do_print(JSON.stringify(status));
+  equal(status[0].name, "AddonManager: Waiting for providers to shut down.");
+  equal(status[0].state, "Complete");
+  equal(status[1].name, "AddonRepository: async shutdown");
+  equal(status[1].state, "in progress");
+
+  // Now finish our shutdown, and wait for the manager to wrap up
+  mockRepo.doneResolve();
+  yield managerDown;
+
+  // Check the shutdown state again
+  status = MockAsyncShutdown.status();
+  equal(status[0].name, "AddonRepository: async shutdown");
+  equal(status[0].state, "done");
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -9,13 +9,14 @@ support-files =
   xpcshell-shared.ini
 
 [test_addon_path_service.js]
 [test_asyncBlocklistLoad.js]
 [test_DeferredSave.js]
 [test_metadata_update.js]
 [test_openh264.js]
 run-if = appname == "firefox"
+[test_provider_shutdown.js]
 [test_shutdown.js]
 [test_XPIcancel.js]
 [test_XPIStates.js]
 
 [include:xpcshell-shared.ini]