Bug 782881 - Protect against attempts to use the Add-ons Manager APIs after shutdown. r=Unfocused
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 10 May 2012 11:33:02 -0700
changeset 102526 d33742ea515f9f5ebc5abef78923630e1804ec97
parent 102525 7a847385e42bcdc4b3807fba6e333e29659f14dd
child 102527 51471fc062ec5614bc2aa5ec38a11c52db9b589c
push id23293
push userttaubert@mozilla.com
push dateThu, 16 Aug 2012 18:11:24 +0000
treeherdermozilla-central@88ba578cbaab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused
bugs782881
milestone17.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 782881 - Protect against attempts to use the Add-ons Manager APIs after shutdown. r=Unfocused Bug 782881 - Protect against attempts to use the Add-ons Manager APIs after shutdown. r=Unfocused
services/sync/tests/unit/test_prefs_store.js
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/addonManager.js
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_bug514327_3.js
toolkit/mozapps/extensions/test/xpcshell/test_bug616841.js
toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -2,16 +2,19 @@ Cu.import("resource://services-sync/engi
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const PREFS_GUID = CommonUtils.encodeBase64URL(Services.appinfo.ID);
 
+loadAddonTestFunctions();
+startupManager();
+
 function makePersona(id) {
   return {
     id: id || Math.random().toString(),
     name: Math.random().toString(),
     headerURL: "http://localhost:1234/a"
   };
 }
 
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -362,16 +362,17 @@ function AddonType(aID, aLocaleURI, aLoc
     });
   }
   else {
     this.name = aLocaleKey;
   }
 }
 
 var gStarted = false;
+var gStartupComplete = false;
 var gCheckCompatibility = true;
 var gStrictCompatibility = true;
 var gCheckUpdateSecurityDefault = true;
 var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
 var gUpdateEnabled = true;
 var gAutoUpdateDefault = true;
 var gHotfixID = null;
 
@@ -438,16 +439,18 @@ var AddonManagerInternal = {
   /**
    * Initializes the AddonManager, loading any known providers and initializing
    * them.
    */
   startup: function AMI_startup() {
     if (gStarted)
       return;
 
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+
     let appChanged = undefined;
 
     let oldAppVersion = null;
     try {
       oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
       appChanged = Services.appinfo.version != oldAppVersion;
     }
     catch (e) { }
@@ -530,26 +533,29 @@ var AddonManagerInternal = {
         Components.utils.import(url, {});
       }
       catch (e) {
         ERROR("Exception loading provider " + entry + " from category \"" +
               url + "\"", e);
       }
     }
 
+    // Once we start calling providers we must allow all normal methods to work.
+    gStarted = true;
+
     this.callProviders("startup", 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];
     }
 
-    gStarted = true;
+    gStartupComplete = true;
   },
 
   /**
    * Registers a new AddonProvider.
    *
    * @param  aProvider
    *         The provider to register
    * @param  aTypes
@@ -662,40 +668,51 @@ var AddonManagerInternal = {
     }
   },
 
   /**
    * Shuts down the addon manager and all registered providers, this must clean
    * up everything in order for automated tests to fake restarts.
    */
   shutdown: function AMI_shutdown() {
+    LOG("shutdown");
+    Services.obs.removeObserver(this, "xpcom-shutdown");
     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);
 
-    this.callProviders("shutdown");
+    // Always clean up listeners, but only shutdown providers if they've been 
+    // started.
+    if (gStarted)
+      this.callProviders("shutdown");
 
     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;
   },
 
   /**
    * Notified when a preference we're interested in has changed.
    *
    * @see nsIObserver
    */
   observe: function AMI_observe(aSubject, aTopic, aData) {
+    if (aTopic == "xpcom-shutdown") {
+      this.shutdown();
+      return;
+    }
+
     switch (aData) {
       case PREF_EM_CHECK_COMPATIBILITY: {
         let oldValue = gCheckCompatibility;
         try {
           gCheckCompatibility = Services.prefs.getBoolPref(PREF_EM_CHECK_COMPATIBILITY);
         } catch(e) {
           gCheckCompatibility = true;
         }
@@ -847,16 +864,20 @@ var AddonManagerInternal = {
     return uri.replace(/\+/g, "%2B");
   },
 
   /**
    * Performs a background update check by starting an update for all add-ons
    * that can be updated.
    */
   backgroundUpdateCheck: function AMI_backgroundUpdateCheck() {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     let hotfixID = this.hotfixID;
 
     let checkHotfix = hotfixID &&
                       Services.prefs.getBoolPref(PREF_APP_UPDATE_ENABLED) &&
                       Services.prefs.getBoolPref(PREF_APP_UPDATE_AUTO);
 
     if (!this.updateEnabled && !checkHotfix)
       return;
@@ -1028,17 +1049,17 @@ var AddonManagerInternal = {
     if (!aType || typeof aType != "string")
       throw Components.Exception("aType must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (!aID || typeof aID != "string")
       throw Components.Exception("aID must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
-    if (gStarted)
+    if (gStartupComplete)
       return;
 
     // Ensure that an ID is only listed in one type of change
     for (let type in this.startupChanges)
       this.removeStartupChange(type, aID);
 
     if (!(aType in this.startupChanges))
       this.startupChanges[aType] = [];
@@ -1057,33 +1078,37 @@ var AddonManagerInternal = {
     if (!aType || typeof aType != "string")
       throw Components.Exception("aType must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (!aID || typeof aID != "string")
       throw Components.Exception("aID must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
-    if (gStarted)
+    if (gStartupComplete)
       return;
 
     if (!(aType in this.startupChanges))
       return;
 
     this.startupChanges[aType] = this.startupChanges[aType].filter(function(aItem) aItem != aID);
   },
 
   /**
    * Calls all registered AddonManagerListeners with an event. Any parameters
    * after the method parameter are passed to the listener.
    *
    * @param  aMethod
    *         The method on the listeners to call
    */
   callManagerListeners: function AMI_callManagerListeners(aMethod, ...aArgs) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aMethod || typeof aMethod != "string")
       throw Components.Exception("aMethod must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     let managerListeners = this.managerListeners.slice(0);
     for (let listener of managerListeners) {
       try {
         if (aMethod in listener)
@@ -1101,16 +1126,20 @@ var AddonManagerInternal = {
    *
    * @param  aMethod
    *         The method on the listeners to call
    * @param  aExtraListeners
    *         An optional array of extra InstallListeners to also call
    * @return false if any of the listeners returned false, true otherwise
    */
   callInstallListeners: function AMI_callInstallListeners(aMethod, aExtraListeners, ...aArgs) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aMethod || typeof aMethod != "string")
       throw Components.Exception("aMethod must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (aExtraListeners && !Array.isArray(aExtraListeners))
       throw Components.Exception("aExtraListeners must be an array or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1138,16 +1167,20 @@ var AddonManagerInternal = {
   /**
    * Calls all registered AddonListeners with an event. Any parameters after
    * the method parameter are passed to the listener.
    *
    * @param  aMethod
    *         The method on the listeners to call
    */
   callAddonListeners: function AMI_callAddonListeners(aMethod, ...aArgs) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aMethod || typeof aMethod != "string")
       throw Components.Exception("aMethod must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     let addonListeners = this.addonListeners.slice(0);
     for (let listener of addonListeners) {
       try {
         if (aMethod in listener)
@@ -1168,16 +1201,20 @@ var AddonManagerInternal = {
    *         The ID of the enabled add-on
    * @param  aType
    *         The type of the enabled add-on
    * @param  aPendingRestart
    *         A boolean indicating if the change will only take place the next
    *         time the application is restarted
    */
   notifyAddonChanged: function AMI_notifyAddonChanged(aID, aType, aPendingRestart) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (aID && typeof aID != "string")
       throw Components.Exception("aID must be a string or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (!aType || typeof aType != "string")
       throw Components.Exception("aType must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1185,27 +1222,35 @@ var AddonManagerInternal = {
   },
 
   /**
    * Notifies all providers they need to update the appDisabled property for
    * their add-ons in response to an application change such as a blocklist
    * update.
    */
   updateAddonAppDisabledStates: function AMI_updateAddonAppDisabledStates() {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     this.callProviders("updateAddonAppDisabledStates");
   },
   
   /**
    * Notifies all providers that the repository has updated its data for
    * installed add-ons.
    *
    * @param  aCallback
    *         Function to call when operation is complete.
    */
   updateAddonRepositoryData: function AMI_updateAddonRepositoryData(aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     new AsyncObjectCaller(this.providers, "updateAddonRepositoryData", {
       nextObject: function(aCaller, aProvider) {
         callProvider(aProvider,
                      "updateAddonRepositoryData",
@@ -1237,16 +1282,20 @@ var AddonManagerInternal = {
    *         An optional placeholder version while the add-on is being downloaded
    * @param  aLoadGroup
    *         An optional nsILoadGroup to associate any network requests with
    * @throws if the aUrl, aCallback or aMimetype arguments are not specified
    */
   getInstallForURL: function AMI_getInstallForURL(aUrl, aCallback, aMimetype,
                                                   aHash, aName, aIconURL,
                                                   aVersion, aLoadGroup) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aUrl || typeof aUrl != "string")
       throw Components.Exception("aURL must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1295,16 +1344,20 @@ var AddonManagerInternal = {
    *         The nsIFile where the add-on is located
    * @param  aCallback
    *         A callback to pass the AddonInstall to
    * @param  aMimetype
    *         An optional mimetype hint for the add-on
    * @throws if the aFile or aCallback arguments are not specified
    */
   getInstallForFile: function AMI_getInstallForFile(aFile, aCallback, aMimetype) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!(aFile instanceof Ci.nsIFile))
       throw Components.Exception("aFile must be a nsIFile",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1335,16 +1388,20 @@ var AddonManagerInternal = {
    *
    * @param  aTypes
    *         An optional array of types to retrieve. Each type is a string name
    * @param  aCallback
    *         A callback which will be passed an array of AddonInstalls
    * @throws If the aCallback argument is not specified
    */
   getInstallsByTypes: function AMI_getInstallsByTypes(aTypes, aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (aTypes && !Array.isArray(aTypes))
       throw Components.Exception("aTypes must be an array or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1367,27 +1424,35 @@ var AddonManagerInternal = {
 
   /**
    * Asynchronously gets all current AddonInstalls.
    *
    * @param  aCallback
    *         A callback which will be passed an array of AddonInstalls
    */
   getAllInstalls: function AMI_getAllInstalls(aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     this.getInstallsByTypes(null, aCallback);
   },
 
   /**
    * Checks whether installation is enabled for a particular mimetype.
    *
    * @param  aMimetype
    *         The mimetype to check
    * @return true if installation is enabled for the mimetype
    */
   isInstallEnabled: function AMI_isInstallEnabled(aMimetype) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aMimetype || typeof aMimetype != "string")
       throw Components.Exception("aMimetype must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     let providers = this.providers.slice(0);
     for (let provider of providers) {
       if (callProvider(provider, "supportsMimetype", false, aMimetype) &&
           callProvider(provider, "isInstallEnabled"))
@@ -1402,16 +1467,20 @@ var AddonManagerInternal = {
    *
    * @param  aMimetype
    *         The mimetype of the add-on
    * @param  aURI
    *         The optional nsIURI of the source
    * @return true if the source is allowed to install this mimetype
    */
   isInstallAllowed: function AMI_isInstallAllowed(aMimetype, aURI) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aMimetype || typeof aMimetype != "string")
       throw Components.Exception("aMimetype must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (aURI && !(aURI instanceof Ci.nsIURI))
       throw Components.Exception("aURI must be a nsIURI or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1436,16 +1505,20 @@ var AddonManagerInternal = {
    *         The optional nsIURI that started the installs
    * @param  aInstalls
    *         The array of AddonInstalls to be installed
    */
   installAddonsFromWebpage: function AMI_installAddonsFromWebpage(aMimetype,
                                                                   aSource,
                                                                   aURI,
                                                                   aInstalls) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aMimetype || typeof aMimetype != "string")
       throw Components.Exception("aMimetype must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (aSource && !(aSource instanceof Ci.nsIDOMWindow))
       throw Components.Exception("aSource must be a nsIDOMWindow or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1539,16 +1612,20 @@ var AddonManagerInternal = {
    *
    * @param  aID
    *         The ID of the add-on to retrieve
    * @param  aCallback
    *         The callback to pass the retrieved add-on to
    * @throws if the aID or aCallback arguments are not specified
    */
   getAddonByID: function AMI_getAddonByID(aID, aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aID || typeof aID != "string")
       throw Components.Exception("aID must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1573,16 +1650,20 @@ var AddonManagerInternal = {
    *
    * @param  aGUID
    *         String GUID of add-on to retrieve
    * @param  aCallback
    *         The callback to pass the retrieved add-on to.
    * @throws if the aGUID or aCallback arguments are not specified
    */
   getAddonBySyncGUID: function AMI_getAddonBySyncGUID(aGUID, aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!aGUID || typeof aGUID != "string")
       throw Components.Exception("aGUID must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1608,16 +1689,20 @@ var AddonManagerInternal = {
    *
    * @param  aIDs
    *         The array of IDs to retrieve
    * @param  aCallback
    *         The callback to pass an array of Addons to
    * @throws if the aID or aCallback arguments are not specified
    */
   getAddonsByIDs: function AMI_getAddonsByIDs(aIDs, aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (!Array.isArray(aIDs))
       throw Components.Exception("aIDs must be an array",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1642,16 +1727,20 @@ var AddonManagerInternal = {
    *
    * @param  aTypes
    *         An optional array of types to retrieve. Each type is a string name
    * @param  aCallback
    *         The callback to pass an array of Addons to.
    * @throws if the aCallback argument is not specified
    */
   getAddonsByTypes: function AMI_getAddonsByTypes(aTypes, aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (aTypes && !Array.isArray(aTypes))
       throw Components.Exception("aTypes must be an array or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1674,31 +1763,43 @@ var AddonManagerInternal = {
 
   /**
    * Asynchronously gets all installed add-ons.
    *
    * @param  aCallback
    *         A callback which will be passed an array of Addons
    */
   getAllAddons: function AMI_getAllAddons(aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
+    if (typeof aCallback != "function")
+      throw Components.Exception("aCallback must be a function",
+                                 Cr.NS_ERROR_INVALID_ARG);
+
     this.getAddonsByTypes(null, aCallback);
   },
 
   /**
    * Asynchronously gets add-ons that have operations waiting for an application
    * restart to complete.
    *
    * @param  aTypes
    *         An optional array of types to retrieve. Each type is a string name
    * @param  aCallback
    *         The callback to pass the array of Addons to
    * @throws if the aCallback argument is not specified
    */
   getAddonsWithOperationsByTypes:
   function AMI_getAddonsWithOperationsByTypes(aTypes, aCallback) {
+    if (!gStarted)
+      throw Components.Exception("AddonManager is not initialized",
+                                 Cr.NS_ERROR_NOT_INITIALIZED);
+
     if (aTypes && !Array.isArray(aTypes))
       throw Components.Exception("aTypes must be an array or null",
                                  Cr.NS_ERROR_INVALID_ARG);
 
     if (typeof aCallback != "function")
       throw Components.Exception("aCallback must be a function",
                                  Cr.NS_ERROR_INVALID_ARG);
 
@@ -1775,17 +1876,16 @@ var AddonManagerInternal = {
    * @param  aListener
    *         The AddonListener to remove
    */
   removeAddonListener: function AMI_removeAddonListener(aListener) {
     if (!aListener || typeof aListener != "object")
       throw Components.Exception("aListener must be an AddonListener object",
                                  Cr.NS_ERROR_INVALID_ARG);
 
-
     let pos = 0;
     while (pos < this.addonListeners.length) {
       if (this.addonListeners[pos] == aListener)
         this.addonListeners.splice(pos, 1);
       else
         pos++;
     }
   },
@@ -1915,20 +2015,16 @@ var AddonManagerPrivate = {
   registerProvider: function AMP_registerProvider(aProvider, aTypes) {
     AddonManagerInternal.registerProvider(aProvider, aTypes);
   },
 
   unregisterProvider: function AMP_unregisterProvider(aProvider) {
     AddonManagerInternal.unregisterProvider(aProvider);
   },
 
-  shutdown: function AMP_shutdown() {
-    AddonManagerInternal.shutdown();
-  },
-
   backgroundUpdateCheck: function AMP_backgroundUpdateCheck() {
     AddonManagerInternal.backgroundUpdateCheck();
   },
 
   addStartupChange: function AMP_addStartupChange(aType, aID) {
     AddonManagerInternal.addStartupChange(aType, aID);
   },
 
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -44,29 +44,18 @@ function amManager() {
 
   messageManager.addMessageListener(MSG_INSTALL_ENABLED, this);
   messageManager.addMessageListener(MSG_INSTALL_ADDONS, this);
   messageManager.loadFrameScript(CHILD_SCRIPT, true, true);
 }
 
 amManager.prototype = {
   observe: function AMC_observe(aSubject, aTopic, aData) {
-    let os = Cc["@mozilla.org/observer-service;1"].
-             getService(Ci.nsIObserverService);
-
-    switch (aTopic) {
-    case "addons-startup":
-      os.addObserver(this, "xpcom-shutdown", false);
+    if (aTopic == "addons-startup")
       AddonManagerPrivate.startup();
-      break;
-    case "xpcom-shutdown":
-      os.removeObserver(this, "xpcom-shutdown");
-      AddonManagerPrivate.shutdown();
-      break;
-    }
   },
 
   /**
    * @see amIWebInstaller.idl
    */
   isInstallEnabled: function AMC_isInstallEnabled(aMimetype, aReferer) {
     return AddonManager.isInstallEnabled(aMimetype);
   },
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -388,17 +388,18 @@ function shutdownManager() {
   obs.addObserver({
     observe: function(aSubject, aTopic, aData) {
       repositoryShutdown = true;
       obs.removeObserver(this, "addon-repository-shutdown");
     }
   }, "addon-repository-shutdown", false);
 
   obs.notifyObservers(null, "quit-application-granted", null);
-  gInternalManager.observe(null, "xpcom-shutdown", null);
+  let scope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
+  scope.AddonManagerInternal.shutdown();
   gInternalManager = null;
 
   AddonRepository.shutdown();
 
   // Load the add-ons list as it was after application shutdown
   loadAddonsList();
 
   // Clear any crash report annotations
@@ -411,17 +412,17 @@ function shutdownManager() {
   // Wait until we observe the shutdown notifications
   while (!repositoryShutdown || !xpiShutdown) {
     if (thr.hasPendingEvents())
       thr.processNextEvent(false);
   }
 
   // Force the XPIProvider provider to reload to better
   // simulate real-world usage.
-  let scope = Components.utils.import("resource://gre/modules/XPIProvider.jsm");
+  scope = Components.utils.import("resource://gre/modules/XPIProvider.jsm");
   AddonManagerPrivate.unregisterProvider(scope.XPIProvider);
   Components.utils.unload("resource://gre/modules/XPIProvider.jsm");
 }
 
 function loadAddonsList() {
   function readDirectories(aSection) {
     var dirs = [];
     var keys = parser.getKeys(aSection);
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_3.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_3.js
@@ -115,16 +115,17 @@ function do_update_blocklist(aDatafile, 
 
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   gTestserver = new HttpServer();
   gTestserver.registerDirectory("/data/", do_get_file("data"));
   gTestserver.start(4444);
 
+  startupManager();
 
   // initialize the blocklist with no entries
   var blocklistFile = gProfD.clone();
   blocklistFile.append("blocklist.xml");
   if (blocklistFile.exists())
     blocklistFile.remove(false);
   var source = do_get_file("data/test_bug514327_3_empty.xml");
   source.copyTo(gProfD, "blocklist.xml");
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug616841.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug616841.js
@@ -8,16 +8,17 @@ function test_string_compare() {
   do_check_true("C".localeCompare("D") < 0);
   do_check_true("D".localeCompare("C") > 0);
   do_check_true("\u010C".localeCompare("D") < 0);
   do_check_true("D".localeCompare("\u010C") > 0);
 }
 
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  startupManager();
 
   do_test_pending();
 
   test_string_compare();
 
   AddonManager.getAddonByID("foo", function(aAddon) {
     test_string_compare();
     do_test_finished();
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Verify that API functions fail if the Add-ons Manager isn't initialised.
+
+const IGNORE = ["escapeAddonURI", "shouldAutoUpdate", "getStartupChanges",
+                "addTypeListener", "removeTypeListener",
+                "addAddonListener", "removeAddonListener",
+                "addInstallListener", "removeInstallListener",
+                "addManagerListener", "removeManagerListener"];
+
+const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
+                        "AddonScreenshot", "AddonType", "startup", "shutdown",
+                        "registerProvider", "unregisterProvider",
+                        "addStartupChange", "removeStartupChange"];
+
+function test_functions() {
+  for (let prop in AddonManager) {
+    if (typeof AddonManager[prop] != "function")
+      continue;
+    if (IGNORE.indexOf(prop) != -1)
+      continue;
+
+    try {
+      do_print("AddonManager." + prop);
+      AddonManager[prop]();
+      do_throw(prop + " did not throw an exception");
+    }
+    catch (e) {
+      if (e.result != Components.results.NS_ERROR_NOT_INITIALIZED)
+        do_throw(prop + " threw an unexpected exception: " + e);
+    }
+  }
+
+  for (let prop in AddonManagerPrivate) {
+    if (typeof AddonManagerPrivate[prop] != "function")
+      continue;
+    if (IGNORE_PRIVATE.indexOf(prop) != -1)
+      continue;
+
+    try {
+      do_print("AddonManagerPrivate." + prop);
+      AddonManagerPrivate[prop]();
+      do_throw(prop + " did not throw an exception");
+    }
+    catch (e) {
+      if (e.result != Components.results.NS_ERROR_NOT_INITIALIZED)
+        do_throw(prop + " threw an unexpected exception: " + e);
+    }
+  }
+}
+
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  test_functions();
+  startupManager();
+  shutdownManager();
+  test_functions();
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -188,16 +188,17 @@ skip-if = os == "android"
 [test_permissions.js]
 [test_plugins.js]
 [test_pluginBlocklistCtp.js]
 # Bug 676992: test consistently fails on Android
 fail-if = os == "android"
 [test_pref_properties.js]
 [test_registry.js]
 [test_safemode.js]
+[test_shutdown.js]
 [test_startup.js]
 # Bug 676992: test consistently fails on Android
 fail-if = os == "android"
 [test_syncGUID.js]
 [test_strictcompatibility.js]
 [test_targetPlatforms.js]
 [test_theme.js]
 # Bug 676992: test consistently fails on Android