Bug 1356826: Part 1 - Uninstall temporary add-ons at shutdown rather than startup. r?rhelmer draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 21 Apr 2017 20:31:08 -0700
changeset 566754 f91483c6a95a0d882099d53b404b57ec1c3f8066
parent 563270 c39985abd71ceaa90fef9c438cc7627667d2e024
child 566755 1dd5c7cd6e6dc71f2c458963420cf422650800c8
push id55320
push usermaglione.k@gmail.com
push dateSun, 23 Apr 2017 05:18:40 +0000
reviewersrhelmer
bugs1356826
milestone55.0a1
Bug 1356826: Part 1 - Uninstall temporary add-ons at shutdown rather than startup. r?rhelmer MozReview-Commit-ID: LQLsqEayEUU
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -94,16 +94,22 @@ XPCOMUtils.defineLazyGetter(this, "IconD
 });
 
 
 Cu.importGlobalProperties(["URL"]);
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
+function getFile(descriptor) {
+  let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+  file.persistentDescriptor = descriptor;
+  return file;
+}
+
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
 const PREF_INSTALL_CACHE              = "extensions.installCache";
 const PREF_XPI_STATE                  = "extensions.xpiState";
 const PREF_BOOTSTRAP_ADDONS           = "extensions.bootstrappedAddons";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
 const PREF_SKIN_SWITCHPENDING         = "extensions.dss.switchPending";
 const PREF_SKIN_TO_SELECT             = "extensions.lastSelectedSkin";
 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
@@ -2245,18 +2251,18 @@ XPIState.prototype = {
         XPIDatabase.saveChanges();
       }
     }
   },
 };
 
 // Constructor for an ES6 Map that knows how to convert itself into a
 // regular object for toJSON().
-function SerializableMap() {
-  let m = new Map();
+function SerializableMap(arg) {
+  let m = new Map(arg);
   m.toJSON = function() {
     let out = {}
     for (let [key, val] of m) {
       out[key] = val;
     }
     return out;
   };
   return m;
@@ -2432,17 +2438,20 @@ this.XPIStates = {
     XPIProvider.setTelemetry(aAddon.id, "location", aAddon.location);
   },
 
   /**
    * Save the current state of installed add-ons.
    * XXX this *totally* should be a .json file using DeferredSave...
    */
   save() {
-    let cache = JSON.stringify(this.db);
+    let db = new SerializableMap(this.db);
+    db.delete(TemporaryInstallLocation.name);
+
+    let cache = JSON.stringify(db);
     Services.prefs.setCharPref(PREF_XPI_STATE, cache);
   },
 
   /**
    * Remove the XPIState for an add-on and save the new state.
    * @param aLocation  The name of the add-on location.
    * @param aId        The ID of the add-on.
    */
@@ -2894,18 +2903,17 @@ this.XPIProvider = {
         this.addAddonsToCrashReporter();
       }
 
       try {
         AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin");
 
         for (let addon of this.sortBootstrappedAddons()) {
           try {
-            let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-            file.persistentDescriptor = addon.descriptor;
+            let file = getFile(addon.descriptor);
             let reason = BOOTSTRAP_REASONS.APP_STARTUP;
             // Eventually set INSTALLED reason when a bootstrap addon
             // is dropped in profile folder and automatically installed
             if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
                             .indexOf(addon.id) !== -1)
               reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
             this.callBootstrapMethod(createAddonDetails(addon.id, addon),
                                      file, "startup", reason);
@@ -2927,18 +2935,17 @@ this.XPIProvider = {
           XPIProvider._closing = true;
           for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) {
             // If no scope has been loaded for this add-on then there is no need
             // to shut it down (should only happen when a bootstrapped add-on is
             // pending enable)
             if (!XPIProvider.activeAddons.has(addon.id))
               continue;
 
-            let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-            file.persistentDescriptor = addon.descriptor;
+            let file = getFile(addon.descriptor);
             let addonDetails = createAddonDetails(addon.id, addon);
 
             // If the add-on was pending disable then shut it down and remove it
             // from the persisted data.
             if (addon.disable) {
               XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown",
                                               BOOTSTRAP_REASONS.ADDON_DISABLE);
               delete XPIProvider.bootstrappedAddons[addon.id];
@@ -2983,16 +2990,43 @@ this.XPIProvider = {
    *                          0 otherwise.
    */
   shutdown() {
     logger.debug("shutdown");
 
     // Stop anything we were doing asynchronously
     this.cancelAll();
 
+    // Uninstall any temporary add-ons.
+    let tempLocation = XPIStates.getLocation(TemporaryInstallLocation.name);
+    if (tempLocation) {
+      for (let [id, addon] of tempLocation.entries()) {
+        tempLocation.delete(id);
+
+        let file = getFile(addon.descriptor);
+
+        this.callBootstrapMethod(createAddonDetails(id, this.bootstrappedAddons[id]),
+                                 file, "uninstall",
+                                 BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+        this.unloadBootstrapScope(id);
+        TemporaryInstallLocation.uninstallAddon(id);
+
+        let [locationName, ] = XPIStates.findAddon(id);
+        if (locationName) {
+          let newAddon = XPIDatabase.makeAddonLocationVisible(id, locationName);
+
+          let file = getFile(newAddon.descriptor);
+
+          this.callBootstrapMethod(createAddonDetails(id, newAddon),
+                                   file, "install",
+                                   BOOTSTRAP_REASONS.ADDON_INSTALL);
+        }
+      }
+    }
+
     this.bootstrappedAddons = {};
     this.activeAddons.clear();
     this.enabledAddons = null;
     this.allAppGlobal = true;
 
     // If there are pending operations then we must update the list of active
     // add-ons
     if (Preferences.get(PREF_PENDING_OPERATIONS, false)) {
@@ -3497,18 +3531,17 @@ this.XPIProvider = {
         seenFiles.push(jsonfile.leafName);
 
         existingAddonID = addon.existingAddonID || id;
 
         var oldBootstrap = null;
         logger.debug("Processing install of " + id + " in " + location.name);
         if (existingAddonID in this.bootstrappedAddons) {
           try {
-            var existingAddon = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
-            existingAddon.persistentDescriptor = this.bootstrappedAddons[existingAddonID].descriptor;
+            var existingAddon = getFile(this.bootstrappedAddons[existingAddonID].descriptor);
             if (existingAddon.exists()) {
               oldBootstrap = this.bootstrappedAddons[existingAddonID];
 
               // We'll be replacing a currently active bootstrapped add-on so
               // call its uninstall method
               let newVersion = addon.version;
               let oldVersion = oldBootstrap.version;
               let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -59,16 +59,17 @@ const PREF_EM_AUTO_DISABLED_SCOPES    = 
 const PREF_E10S_BLOCKED_BY_ADDONS     = "extensions.e10sBlockedByAddons";
 const PREF_E10S_MULTI_BLOCKED_BY_ADDONS = "extensions.e10sMultiBlockedByAddons";
 const PREF_E10S_HAS_NONEXEMPT_ADDON   = "extensions.e10s.rollout.hasAddon";
 
 const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_SYSTEM_ADDONS           = "app-system-addons";
 const KEY_APP_SYSTEM_DEFAULTS         = "app-system-defaults";
 const KEY_APP_GLOBAL                  = "app-global";
+const KEY_APP_TEMPORARY               = "app-temporary";
 
 // Properties that only exist in the database
 const DB_METADATA        = ["syncGUID",
                             "installDate",
                             "updateDate",
                             "size",
                             "sourceURI",
                             "releaseNotesURI",
@@ -483,17 +484,18 @@ this.XPIDatabase = {
   toJSON() {
     if (!this.addonDB) {
       // We never loaded the database?
       throw new Error("Attempt to save database without loading it first");
     }
 
     let toSave = {
       schemaVersion: DB_SCHEMA,
-      addons: [...this.addonDB.values()]
+      addons: Array.from(this.addonDB.values())
+                   .filter(addon => addon.location != KEY_APP_TEMPORARY),
     };
     return toSave;
   },
 
   /**
    * Pull upgrade information from an existing SQLITE database
    *
    * @return false if there is no SQLITE database
@@ -1303,16 +1305,45 @@ this.XPIDatabase = {
         otherAddon.active = false;
       }
     }
     aAddon.visible = true;
     this.saveChanges();
   },
 
   /**
+   * Synchronously marks a given add-on ID visible in a given location,
+   * instances with the same ID as not visible.
+   *
+   * @param  aAddon
+   *         The DBAddonInternal to make visible
+   */
+  makeAddonLocationVisible(aId, aLocation) {
+    logger.debug(`Make addon ${aId} visible in location ${aLocation}`);
+    let result;
+    for (let [, addon] of this.addonDB) {
+      if (addon.id != aId) {
+        continue;
+      }
+      if (addon.location == aLocation) {
+        logger.debug("Reveal addon " + addon._key);
+        addon.visible = true;
+        addon.active = true;
+        result = addon;
+      } else {
+        logger.debug("Hide addon " + addon._key);
+        addon.visible = false;
+        addon.active = false;
+      }
+    }
+    this.saveChanges();
+    return result;
+  },
+
+  /**
    * Synchronously sets properties for an add-on.
    *
    * @param  aAddon
    *         The DBAddonInternal being updated
    * @param  aProperties
    *         A dictionary of properties to set
    */
   setAddonProperties(aAddon, aProperties) {
--- a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
@@ -43,17 +43,17 @@ const BOOTSTRAP_REASONS = {
 function waitForBootstrapEvent(expectedEvent, addonId) {
   return new Promise(resolve => {
     const observer = {
       observe: (subject, topic, data) => {
         const info = JSON.parse(data);
         const targetAddonId = info.data.id;
         if (targetAddonId === addonId && info.event === expectedEvent) {
           resolve(info);
-          Services.obs.removeObserver(observer);
+          Services.obs.removeObserver(observer, "bootstrapmonitor-event");
         } else {
           do_print(
             `Ignoring bootstrap event: ${info.event} for ${targetAddonId}`);
         }
       },
     };
     Services.obs.addObserver(observer, "bootstrapmonitor-event");
   });