Bug 1059674 - Use AsynchShutdown.blocker() for AddonManager provider shutdown. r=Unfocused, r=Yoric, r=Mossop, a=sledru
authorIrving Reid <irving@mozilla.com>
Sat, 11 Oct 2014 10:14:40 -0400
changeset 218104 bda37eb8a921
parent 218099 45dd53a5354b
child 218105 b3ce9237bb9a
push id542
push userryanvm@gmail.com
push date2014-10-22 19:27 +0000
treeherdermozilla-release@b3ce9237bb9a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused, Yoric, Mossop, sledru
bugs1059674
milestone33.0.1
Bug 1059674 - Use AsynchShutdown.blocker() for AddonManager provider shutdown. r=Unfocused, r=Yoric, r=Mossop, a=sledru
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
@@ -2272,16 +2272,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()");
     Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
--- 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: function(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),
+                                                   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: function() {
+    let state = [];
+    if (gShutdownBarrier) {
+      state.push({
+        name: gShutdownBarrier._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
@@ -1593,16 +1593,18 @@ function recordAddonTelemetry(aAddon) {
     if (loc.name)
       XPIProvider.setTelemetry(aAddon.id, "name", loc.name);
     if (loc.creator)
       XPIProvider.setTelemetry(aAddon.id, "creator", loc.creator);
   }
 }
 
 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,44 @@ 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;
+      if (!aOptions || (typeof(aOptions) == "function")) {
+        MockAsyncShutdown.status = aOptions;
+      } else {
+        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: function() {
+      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
@@ -10,9 +10,10 @@ support-files =
 [include: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_XPIcancel.js]