Bug 1089695 - Async sanitize.js;r=mak draft
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Mon, 01 Jun 2015 14:08:43 +0200
changeset 269028 60b83c68ad5520c7bd40bea8c3aaca9c41681f00
parent 269027 7506504483f91c405ba26d71e2244baefe09d1e6
child 506569 74551e4f63a9d315d105a2755517e081b11841c3
push id2440
push userdteller@mozilla.com
push dateMon, 01 Jun 2015 21:16:18 +0000
reviewersmak
bugs1089695
milestone41.0a1
Bug 1089695 - Async sanitize.js;r=mak
browser/base/content/sanitize.js
browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
browser/base/content/test/general/browser_sanitize-sitepermissions.js
toolkit/components/places/PlacesUtils.jsm
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -1,39 +1,59 @@
-# -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+// -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+                                  "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
                                   "resource:///modules/DownloadsCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                   "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+                                  "resource://gre/modules/devtools/Console.jsm");
 
-function Sanitizer() {}
+function Sanitizer() {
+  /**
+   * The list of items currently being cleared, used for error-reporting.
+   *
+   * This object associates names to a human-readable description of the
+   * state of cleaning up the item.
+   */
+  this._itemsToClear = {};
+}
 Sanitizer.prototype = {
   // warning to the caller: this one may raise an exception (e.g. bug #265028)
   clearItem: function (aItemName)
   {
     if (this.items[aItemName].canClear)
       this.items[aItemName].clear();
   },
 
+  promiseCanClearItem: function (aItemName, aArg) {
+    return new Promise(resolve =>
+      this.canClearItem(aItemName,
+        (_, canClear) => resolve(canClear),
+        aArg)
+    );
+  },
+
   canClearItem: function (aItemName, aCallback, aArg)
   {
     let canClear = this.items[aItemName].canClear;
     if (typeof canClear == "function") {
       canClear(aCallback, aArg);
       return false;
     }
 
@@ -52,158 +72,156 @@ Sanitizer.prototype = {
    * Deletes privacy sensitive data in a batch, according to user preferences.
    * Returns a promise which is resolved if no errors occurred.  If an error
    * occurs, a message is reported to the console and all other items are still
    * cleared before the promise is finally rejected.
    *
    * If the consumer specifies the (optional) array parameter, only those
    * items get cleared (irrespective of the preference settings)
    */
-  sanitize: function (aItemsToClear)
+  sanitize: Task.async(function* (aItemsToClear = null)
   {
-    var deferred = Promise.defer();
-    var seenError = false;
+    let seenError = false;
+    let itemsToClear;
     if (Array.isArray(aItemsToClear)) {
-      var itemsToClear = [...aItemsToClear];
+      // Shallow copy the array, as we are going to modify
+      // it in place later.
+      itemsToClear = [...aItemsToClear];
     } else {
       let branch = Services.prefs.getBranch(this.prefDomain);
       itemsToClear = Object.keys(this.items).filter(itemName => branch.getBoolPref(itemName));
     }
 
+    // Store the list of items to clear, for debugging/forensics purposes
+    this._itemsToClear = {};
+    for (let k of itemsToClear) {
+      this._itemsToClear[k] = "ready";
+    }
+
     // Ensure open windows get cleared first, if they're in our list, so that they don't stick
     // around in the recently closed windows list, and so we can cancel the whole thing
     // if the user selects to keep a window open from a beforeunload prompt.
     let openWindowsIndex = itemsToClear.indexOf("openWindows");
     if (openWindowsIndex != -1) {
       itemsToClear.splice(openWindowsIndex, 1);
-      let item = this.items.openWindows;
-
-      let ok = item.clear(() => {
-        try {
-          let clearedPromise = this.sanitize(itemsToClear);
-          clearedPromise.then(deferred.resolve, deferred.reject);
-        } catch(e) {
-          let error = "Sanitizer threw after closing windows: " + e;
-          Cu.reportError(error);
-          deferred.reject(error);
-        }
-      });
-      // When cancelled, reject immediately
-      if (!ok) {
-        deferred.reject("Sanitizer canceled closing windows");
-      }
-
-      return deferred.promise;
+      yield this.items.openWindows.clear();
+      this._itemsToClear.openWindows = "cleared";
     }
 
-    TelemetryStopwatch.start("FX_SANITIZE_TOTAL");
-
     // Cache the range of times to clear
-    if (this.ignoreTimespan)
-      var range = null;  // If we ignore timespan, clear everything
-    else
+    let range = null;
+    // If we ignore timespan, clear everything,
+    // otherwise, pick a range.
+    if (!this.ignoreTimespan) {
       range = this.range || Sanitizer.getClearRange();
+    }
 
-    let itemCount = Object.keys(itemsToClear).length;
-    let onItemComplete = function() {
-      if (!--itemCount) {
-        TelemetryStopwatch.finish("FX_SANITIZE_TOTAL");
-        seenError ? deferred.reject() : deferred.resolve();
-      }
-    };
     for (let itemName of itemsToClear) {
       let item = this.items[itemName];
+      if (!("clear" in item)) {
+        this._itemsToClear[itemName] = '`clear` not in item';
+        continue;
+      }
       item.range = range;
-      if ("clear" in item) {
-        let clearCallback = (itemName, aCanClear) => {
-          // Some of these clear() may raise exceptions (see bug #265028)
-          // to sanitize as much as possible, we catch and store them,
-          // rather than fail fast.
-          // Callers should check returned errors and give user feedback
-          // about items that could not be sanitized
-          let item = this.items[itemName];
-          try {
-            if (aCanClear)
-              item.clear();
-          } catch(er) {
-            seenError = true;
-            Components.utils.reportError("Error sanitizing " + itemName +
-                                         ": " + er + "\n");
-          }
-          onItemComplete();
-        };
-        this.canClearItem(itemName, clearCallback);
-      } else {
-        onItemComplete();
+      let canClear = yield this.promiseCanClearItem(itemName);
+      if (!canClear) {
+        this._itemsToClear[itemName] = "cannot clear item";
+        continue;
+      }
+      // Some of these clear() may raise exceptions (see bug #265028)
+      // to sanitize as much as possible, we catch and store them,
+      // rather than fail fast.
+      // Callers should check returned errors and give user feedback
+      // about items that could not be sanitized
+      let refObj = {};
+      try {
+        TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
+        yield item.clear();
+        this._itemsToClear[itemName] = "cleared";
+      } catch(er) {
+        this._itemsToClear[itemName] = "failed";
+        seenError = true;
+        console.error("Error sanitizing " + itemName, er);
+      } finally {
+        TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
       }
     }
 
-    return deferred.promise;
-  },
+    this._itemsToClear = {};
+    if (seenError) {
+      throw new Error("Error sanitizing");
+    }
+  }),
 
   // Time span only makes sense in certain cases.  Consumers who want
   // to only clear some private data can opt in by setting this to false,
   // and can optionally specify a specific range.  If timespan is not ignored,
   // and range is not set, sanitize() will use the value of the timespan
   // pref to determine a range
   ignoreTimespan : true,
   range : null,
 
   items: {
     cache: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_CACHE");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
 
         var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"].
                     getService(Ci.nsICacheStorageService);
         try {
           // Cache doesn't consult timespan, nor does it have the
           // facility for timespan-based eviction.  Wipe it.
           cache.clear();
         } catch(er) {}
 
         var imageCache = Cc["@mozilla.org/image/tools;1"].
                          getService(Ci.imgITools).getImgCacheForDocument(null);
         try {
           imageCache.clearCache(false); // true=chrome, false=content
         } catch(er) {}
 
-        TelemetryStopwatch.finish("FX_SANITIZE_CACHE");
+        TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
 
     cookies: {
-      clear: function ()
+      clear: Task.async(function* ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_COOKIES");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_COOKIES", refObj);
 
         var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"]
                                   .getService(Ci.nsICookieManager);
         if (this.range) {
           // Iterate through the cookies and delete any created after our cutoff.
           var cookiesEnum = cookieMgr.enumerator;
           while (cookiesEnum.hasMoreElements()) {
             var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
 
-            if (cookie.creationTime > this.range[0])
+            if (cookie.creationTime > this.range[0]) {
               // This cookie was created after our cutoff, clear it
               cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
+              yield Promise.resolve(); // Don't block the main thread too long
+            }
           }
         }
         else {
           // Remove everything
           cookieMgr.removeAll();
         }
 
+        yield Promise.resolve(); // Don't block the main thread too long
+
         // Clear deviceIds. Done asynchronously (returns before complete).
         let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"]
                                  .getService(Ci.nsIMediaManagerService);
         mediaMgr.sanitizeDeviceIds(this.range && this.range[0]);
 
         // Clear plugin data.
         const phInterface = Ci.nsIPluginHost;
         const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL;
@@ -213,95 +231,106 @@ Sanitizer.prototype = {
         // that this.range[1] is actually now, so we compute age range based
         // on the lower bound. If this.range results in a negative age, do
         // nothing.
         let age = this.range ? (Date.now() / 1000 - this.range[0] / 1000000)
                              : -1;
         if (!this.range || age >= 0) {
           let tags = ph.getPluginTags();
           for (let i = 0; i < tags.length; i++) {
+            yield Promise.resolve(); // Don't block the main thread too long
             try {
               ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, age);
             } catch (e) {
               // If the plugin doesn't support clearing by age, clear everything.
               if (e.result == Components.results.
                     NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
                 try {
                   ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, -1);
                 } catch (e) {
                   // Ignore errors from the plugin
                 }
               }
             }
           }
         }
 
-        TelemetryStopwatch.finish("FX_SANITIZE_COOKIES");
-      },
+        TelemetryStopwatch.finish("FX_SANITIZE_COOKIES", refObj);
+      }),
 
       get canClear()
       {
         return true;
       }
     },
 
     offlineApps: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS", refObj);
         Components.utils.import("resource:///modules/offlineAppCache.jsm");
         OfflineAppCacheHelper.clear();
-        TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS");
+        TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
 
     history: {
-      clear: function ()
+      clear: Task.async(function* ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_HISTORY");
-
-        if (this.range)
-          PlacesUtils.history.removeVisitsByTimeframe(this.range[0], this.range[1]);
-        else
-          PlacesUtils.history.removeAllPages();
-
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
         try {
-          var os = Components.classes["@mozilla.org/observer-service;1"]
-                             .getService(Components.interfaces.nsIObserverService);
-          let clearStartingTime = this.range ? String(this.range[0]) : "";
-          os.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
-        }
-        catch (e) { }
+          if (this.range) {
+            let range = {
+              beginDate: new Date(this.range[0] / 1000),
+              endDate: new Date(this.range[1] / 1000)
+            };
+            yield PlacesUtils.history.removeVisitsByFilter(range);
+          } else {
+            // Remove everything.
+            yield PlacesUtils.history.clear();
+          }
 
-        try {
-          var predictor = Components.classes["@mozilla.org/network/predictor;1"]
-                                    .getService(Components.interfaces.nsINetworkPredictor);
-          predictor.reset();
-        } catch (e) { }
+          try {
+            let os = Components.classes["@mozilla.org/observer-service;1"]
+                               .getService(Components.interfaces.nsIObserverService);
+            let clearStartingTime = this.range ? String(this.range[0]) : "";
+            os.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
+          }
+          catch (e) { }
 
-        TelemetryStopwatch.finish("FX_SANITIZE_HISTORY");
-      },
+          try {
+            let predictor = Components.classes["@mozilla.org/network/predictor;1"]
+                                      .getService(Components.interfaces.nsINetworkPredictor);
+            predictor.reset();
+          } catch (e) { }
+        } finally {
+          TelemetryStopwatch.finish("FX_SANITIZE_HISTORY", refObj);
+        }
+      }),
 
       get canClear()
       {
         // bug 347231: Always allow clearing history due to dependencies on
         // the browser:purge-session-history notification. (like error console)
         return true;
       }
     },
 
     formdata: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_FORMDATA");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj);
 
         // Clear undo history of all searchBars
         var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
                                       .getService(Components.interfaces.nsIWindowMediator);
         var windows = windowManager.getEnumerator("navigator:browser");
         while (windows.hasMoreElements()) {
           let currentWindow = windows.getNext();
           let currentDocument = currentWindow.document;
@@ -318,17 +347,17 @@ Sanitizer.prototype = {
         }
 
         let change = { op: "remove" };
         if (this.range) {
           [ change.firstUsedStart, change.firstUsedEnd ] = this.range;
         }
         FormHistory.update(change);
 
-        TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA");
+        TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj);
       },
 
       canClear : function(aCallback, aArg)
       {
         var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
                                       .getService(Components.interfaces.nsIWindowMediator);
         var windows = windowManager.getEnumerator("navigator:browser");
         while (windows.hasMoreElements()) {
@@ -365,92 +394,96 @@ Sanitizer.prototype = {
         FormHistory.count({}, countDone);
         return false;
       }
     },
 
     downloads: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
         Task.spawn(function () {
           let filterByTime = null;
           if (this.range) {
             // Convert microseconds back to milliseconds for date comparisons.
             let rangeBeginMs = this.range[0] / 1000;
             let rangeEndMs = this.range[1] / 1000;
             filterByTime = download => download.startTime >= rangeBeginMs &&
                                        download.startTime <= rangeEndMs;
           }
 
           // Clear all completed/cancelled downloads
           let list = yield Downloads.getList(Downloads.ALL);
           list.removeFinished(filterByTime);
-          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS");
+          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
         }.bind(this)).then(null, error => {
-          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS");
+          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
           Components.utils.reportError(error);
         });
       },
 
       canClear : function(aCallback, aArg)
       {
         aCallback("downloads", true, aArg);
         return false;
       }
     },
 
     passwords: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_PASSWORDS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_PASSWORDS", refObj);
         var pwmgr = Components.classes["@mozilla.org/login-manager;1"]
                               .getService(Components.interfaces.nsILoginManager);
         // Passwords are timeless, and don't respect the timeSpan setting
         pwmgr.removeAllLogins();
-        TelemetryStopwatch.finish("FX_SANITIZE_PASSWORDS");
+        TelemetryStopwatch.finish("FX_SANITIZE_PASSWORDS", refObj);
       },
 
       get canClear()
       {
         var pwmgr = Components.classes["@mozilla.org/login-manager;1"]
                               .getService(Components.interfaces.nsILoginManager);
         var count = pwmgr.countLogins("", "", ""); // count all logins
         return (count > 0);
       }
     },
 
     sessions: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_SESSIONS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj);
 
         // clear all auth tokens
         var sdr = Components.classes["@mozilla.org/security/sdr;1"]
                             .getService(Components.interfaces.nsISecretDecoderRing);
         sdr.logoutAndTeardown();
 
         // clear FTP and plain HTTP auth sessions
         var os = Components.classes["@mozilla.org/observer-service;1"]
                            .getService(Components.interfaces.nsIObserverService);
         os.notifyObservers(null, "net:clear-active-logins", null);
 
-        TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS");
+        TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
 
     siteSettings: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS", refObj);
 
         // Clear site-specific permissions like "Allow this site to open popups"
         // we ignore the "end" range and hope it is now() - none of the
         // interfaces used here support a true range anyway.
         let startDateMS = this.range == null ? null : this.range[0] / 1000;
         var pm = Components.classes["@mozilla.org/permissionmanager;1"]
                            .getService(Components.interfaces.nsIPermissionManager);
         if (startDateMS == null) {
@@ -486,17 +519,17 @@ Sanitizer.prototype = {
                     .getService(Ci.nsISiteSecurityService);
         sss.clearAll();
 
         // Clear all push notification subscriptions
         var push = Cc["@mozilla.org/push/NotificationService;1"]
                     .getService(Ci.nsIPushNotificationService);
         push.clearAll();
 
-        TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS");
+        TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
     openWindows: {
@@ -521,25 +554,33 @@ Sanitizer.prototype = {
         }
         return true;
       },
       _resetAllWindowClosures: function(aWindowList) {
         for (let win of aWindowList) {
           win.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow();
         }
       },
-      clear: function(aCallback)
+      clear: function() {
+        return new Promise((resolve, reject) => {
+          let result = this._clear(() => {
+            // this._clear() succeeds asynchronously...
+            resolve();
+          });
+          // ... but fails synchronously
+          if (!result) {
+            reject(new Error("Could not close windows"));
+          }
+        });
+      },
+      _clear: function(aCallback)
       {
         // NB: this closes all *browser* windows, not other windows like the library, about window,
         // browser console, etc.
 
-        if (!aCallback) {
-          throw "Sanitizer's openWindows clear() requires a callback.";
-        }
-
         // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
         // dialogs
         let existingWindow = Services.appShell.hiddenDOMWindow;
         let startDate = existingWindow.performance.now();
 
         // First check if all these windows are OK with being closed:
         let windowEnumerator = Services.wm.getEnumerator("navigator:browser");
         let windowList = [];
@@ -559,71 +600,73 @@ Sanitizer.prototype = {
           if (existingWindow.performance.now() > (startDate + 60 * 1000)) {
             this._resetAllWindowClosures(windowList);
             return false;
           }
         }
 
         // If/once we get here, we should actually be able to close all windows.
 
-        TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS", refObj);
 
         // First create a new window. We do this first so that on non-mac, we don't
         // accidentally close the app by closing all the windows.
         let handler = Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler);
         let defaultArgs = handler.defaultArgs;
         let features = "chrome,all,dialog=no," + this.privateStateForNewWindow;
         let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank",
                                                   features, defaultArgs);
-#ifdef XP_MACOSX
-        function onFullScreen(e) {
-          newWindow.removeEventListener("fullscreen", onFullScreen);
-          let docEl = newWindow.document.documentElement;
-          let sizemode = docEl.getAttribute("sizemode");
-          if (!newWindow.fullScreen && sizemode == "fullscreen") {
-            docEl.setAttribute("sizemode", "normal");
-            e.preventDefault();
-            e.stopPropagation();
-            return false;
+
+        if (AppConstants.platform == "macosx") {
+          let onFullScreen = function(e) {
+            newWindow.removeEventListener("fullscreen", onFullScreen);
+            let docEl = newWindow.document.documentElement;
+            let sizemode = docEl.getAttribute("sizemode");
+            if (!newWindow.fullScreen && sizemode == "fullscreen") {
+              docEl.setAttribute("sizemode", "normal");
+              e.preventDefault();
+              e.stopPropagation();
+              return false;
+            }
           }
+          newWindow.addEventListener("fullscreen", onFullScreen);
         }
-        newWindow.addEventListener("fullscreen", onFullScreen);
-#endif
 
         // Window creation and destruction is asynchronous. We need to wait
         // until all existing windows are fully closed, and the new window is
         // fully open, before continuing. Otherwise the rest of the sanitizer
         // could run too early (and miss new cookies being set when a page
         // closes) and/or run too late (and not have a fully-formed window yet
         // in existence). See bug 1088137.
         let newWindowOpened = false;
         function onWindowOpened(subject, topic, data) {
           if (subject != newWindow)
             return;
 
           Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished");
-#ifdef XP_MACOSX
-          newWindow.removeEventListener("fullscreen", onFullScreen);
-#endif
+          if (AppConstants.platform == "macosx") {
+            newWindow.removeEventListener("fullscreen", onFullScreen);
+          }
           newWindowOpened = true;
           // If we're the last thing to happen, invoke callback.
           if (numWindowsClosing == 0) {
-            TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS");
+            TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
             aCallback();
           }
         }
 
         let numWindowsClosing = windowList.length;
         function onWindowClosed() {
           numWindowsClosing--;
           if (numWindowsClosing == 0) {
             Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
             // If we're the last thing to happen, invoke callback.
             if (newWindowOpened) {
-              TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS");
+              TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
               aCallback();
             }
           }
         }
 
         Services.obs.addObserver(onWindowOpened, "browser-delayed-startup-finished", false);
         Services.obs.addObserver(onWindowClosed, "xul-window-destroyed", false);
 
@@ -710,21 +753,20 @@ Sanitizer.__defineGetter__("prefs", func
                          .getBranch(Sanitizer.prefDomain);
 });
 
 // Shows sanitization UI
 Sanitizer.showUI = function(aParentWindow)
 {
   var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                      .getService(Components.interfaces.nsIWindowWatcher);
-#ifdef XP_MACOSX
-  ww.openWindow(null, // make this an app-modal window on Mac
-#else
-  ww.openWindow(aParentWindow,
-#endif
+  let win = AppConstants.platform == "macosx" ?
+    null: // make this an app-modal window on Mac
+    aParentWindow;
+  ww.openWindow(win,
                 "chrome://browser/content/sanitize.xul",
                 "Sanitize",
                 "chrome,titlebar,dialog,centerscreen,modal",
                 null);
 };
 
 /**
  * Deletes privacy sensitive data in a batch, optionally showing the
@@ -733,31 +775,45 @@ Sanitizer.showUI = function(aParentWindo
 Sanitizer.sanitize = function(aParentWindow)
 {
   Sanitizer.showUI(aParentWindow);
 };
 
 Sanitizer.onStartup = function()
 {
   // we check for unclean exit with pending sanitization
-  Sanitizer._checkAndSanitize();
+  Sanitizer._checkAndSanitize(true);
 };
 
 Sanitizer.onShutdown = function()
 {
   // we check if sanitization is needed and perform it
-  Sanitizer._checkAndSanitize();
+  Sanitizer._checkAndSanitize(false);
 };
 
 // this is called on startup and shutdown, to perform pending sanitizations
-Sanitizer._checkAndSanitize = function()
+Sanitizer._checkAndSanitize = function(isStartup)
 {
   const prefs = Sanitizer.prefs;
   if (prefs.getBoolPref(Sanitizer.prefShutdown) &&
       !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) {
     // this is a shutdown or a startup after an unclean exit
-    var s = new Sanitizer();
+    let s = new Sanitizer();
     s.prefDomain = "privacy.clearOnShutdown.";
-    s.sanitize().then(function() {
-      prefs.setBoolPref(Sanitizer.prefDidShutdown, true);
-    });
+
+    let promise = s.sanitize();
+    if (!isStartup){
+      promise = promise.then(() => {
+        prefs.setBoolPref(Sanitizer.prefDidShutdown, true);
+      });
+    }
+    PlacesUtils.shutdownClient.addBlocker("sanitize.js: Sanitize on " + (isStartup?"startup":"shutdown"),
+      promise,
+      {
+        fetchState: () => ({
+          isStartup,
+          items: this._itemsToClear
+        })
+      }
+    );
+    return promise;
   }
 };
--- a/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
+++ b/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
@@ -1,23 +1,22 @@
 // Bug 474792 - Clear "Never remember passwords for this site" when
 // clearing site-specific settings in Clear Recent History dialog
 
 let tempScope = {};
 Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
                                            .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
 let Sanitizer = tempScope.Sanitizer;
 
-function test() {
-
+add_task(function*() {
   var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
 
   // Add a disabled host
   pwmgr.setLoginSavingEnabled("http://example.com", false);
-  
+
   // Sanity check
   is(pwmgr.getLoginSavingEnabled("http://example.com"), false,
      "example.com should be disabled for password saving since we haven't cleared that yet.");
 
   // Set up the sanitizer to just clear siteSettings
   let s = new Sanitizer();
   s.ignoreTimespan = false;
   s.prefDomain = "privacy.cpd.";
@@ -26,16 +25,16 @@ function test() {
   itemPrefs.setBoolPref("downloads", false);
   itemPrefs.setBoolPref("cache", false);
   itemPrefs.setBoolPref("cookies", false);
   itemPrefs.setBoolPref("formdata", false);
   itemPrefs.setBoolPref("offlineApps", false);
   itemPrefs.setBoolPref("passwords", false);
   itemPrefs.setBoolPref("sessions", false);
   itemPrefs.setBoolPref("siteSettings", true);
-  
+
   // Clear it
-  s.sanitize();
-  
+  yield s.sanitize();
+
   // Make sure it's gone
   is(pwmgr.getLoginSavingEnabled("http://example.com"), true,
      "example.com should be enabled for password saving again now that we've cleared.");
-}
+});
--- a/browser/base/content/test/general/browser_sanitize-sitepermissions.js
+++ b/browser/base/content/test/general/browser_sanitize-sitepermissions.js
@@ -10,17 +10,17 @@ function countPermissions() {
   let enumerator = Services.perms.enumerator;
   while (enumerator.hasMoreElements()) {
     result++;
     enumerator.getNext();
   }
   return result;
 }
 
-function test() {
+add_task(function* test() {
   // sanitize before we start so we have a good baseline.
   // Set up the sanitizer to just clear siteSettings
   let s = new Sanitizer();
   s.ignoreTimespan = false;
   s.prefDomain = "privacy.cpd.";
   var itemPrefs = gPrefService.getBranch(s.prefDomain);
   itemPrefs.setBoolPref("history", false);
   itemPrefs.setBoolPref("downloads", false);
@@ -40,13 +40,13 @@ function test() {
   // Add a permission entry
   var pm = Services.perms;
   pm.add(makeURI("http://example.com"), "testing", pm.ALLOW_ACTION);
 
   // Sanity check
   ok(pm.enumerator.hasMoreElements(), "Permission manager should have elements, since we just added one");
 
   // Clear it
-  s.sanitize();
+  yield s.sanitize();
 
   // Make sure it's gone
   is(numAtStart, countPermissions(), "Permission manager should have the same count it started with");
-}
+});
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -1416,16 +1416,25 @@ this.PlacesUtils = {
       throw new TypeError("Expecting a user-readable name");
     }
     return Task.spawn(function*() {
       let db = yield gAsyncDBWrapperPromised;
       return db.executeBeforeShutdown(name, task);
     });
   },
 
+  get shutdownClient() {
+    dump(`PlacesUtils.get shutdownClient\n`);
+    let hs = Cc["@mozilla.org/browser/nav-history-service;1"]
+               .getService(Ci.nsINavHistoryService)
+               .QueryInterface(Ci.nsIBrowserHistory)
+               .QueryInterface(Ci.nsPIPlacesDatabase);
+    return hs.shutdownClient.jsclient;
+  },
+
   /**
    * Given a uri returns list of itemIds associated to it.
    *
    * @param aURI
    *        nsIURI or spec of the page.
    * @param aCallback
    *        Function to be called when done.
    *        The function will receive an array of itemIds associated to aURI and