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 102225 d33742ea515f9f5ebc5abef78923630e1804ec97
parent 102224 7a847385e42bcdc4b3807fba6e333e29659f14dd
child 102226 51471fc062ec5614bc2aa5ec38a11c52db9b589c
push id1005
push userbmcbride@mozilla.com
push dateThu, 16 Aug 2012 08:48:10 +0000
treeherderfx-team@d33742ea515f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused
bugs782881
milestone17.0a1
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