Bug 1272652 - Firefox fails to import bookmarks from Chrome if it also imports a large history. r=gijs, a=ritu
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 24 May 2016 17:29:30 +0200
changeset 324499 a49ba1e48c59e5f0ac08becf6439372d2dd52670
parent 324498 2516691981a36595ca0bd33e50aa5508423f33d8
child 324500 860a355f185af9e1d744c38b9a2c9b6815b250b9
push id6038
push userkwierso@gmail.com
push dateTue, 31 May 2016 23:02:48 +0000
treeherdermozilla-beta@cf6ec12bd620 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgijs, ritu
bugs1272652
milestone47.0
Bug 1272652 - Firefox fails to import bookmarks from Chrome if it also imports a large history. r=gijs, a=ritu MozReview-Commit-ID: 3w5TIPi2S8d
browser/components/migration/ChromeProfileMigrator.js
browser/components/migration/MigrationUtils.jsm
browser/components/migration/tests/marionette/test_refresh_firefox.py
toolkit/modules/Sqlite.jsm
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -26,17 +26,18 @@ Cu.import("resource://gre/modules/NetUti
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource:///modules/MigrationUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
                                   "resource://gre/modules/OSCrypto.jsm");
-
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+                                  "resource://gre/modules/Sqlite.jsm");
 /**
  * Get an nsIFile instance representing the expected location of user data
  * for this copy of Chrome/Chromium/Canary on different OSes.
  * @param subfoldersWin {Array} an array of subfolders to use for Windows
  * @param subfoldersOSX {Array} an array of subfolders to use for OS X
  * @param subfoldersUnix {Array} an array of subfolders to use for *nix systems
  * @returns {nsIFile} the place we expect data to live. Might not actually exist!
  */
@@ -285,64 +286,73 @@ function GetHistoryResource(aProfileFold
   let historyFile = aProfileFolder.clone();
   historyFile.append("History");
   if (!historyFile.exists())
     return null;
 
   return {
     type: MigrationUtils.resourceTypes.HISTORY,
 
-    migrate: function(aCallback) {
-      let dbConn = Services.storage.openUnsharedDatabase(historyFile);
-      let stmt = dbConn.createAsyncStatement(
-        "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0");
+    migrate(aCallback) {
+      Task.spawn(function* () {
+        let db = yield Sqlite.openConnection({
+          path: historyFile.path
+        });
 
-      stmt.executeAsync({
-        handleResult : function(aResults) {
-          let places = [];
-          for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
-            try {
-              // if having typed_count, we changes transition type to typed.
-              let transType = PlacesUtils.history.TRANSITION_LINK;
-              if (row.getResultByName("typed_count") > 0)
-                transType = PlacesUtils.history.TRANSITION_TYPED;
+        let rows = yield db.execute(`SELECT url, title, last_visit_time, typed_count
+                                     FROM urls WHERE hidden = 0`);
+        yield db.close();
 
-              places.push({
-                uri: NetUtil.newURI(row.getResultByName("url")),
-                title: row.getResultByName("title"),
-                visits: [{
-                  transitionType: transType,
-                  visitDate: chromeTimeToDate(
-                               row.getResultByName(
-                                 "last_visit_time")) * 1000,
-                }],
-              });
-            } catch (e) {
-              Cu.reportError(e);
-            }
-          }
+        let places = [];
+        for (let row of rows) {
+          try {
+            // if having typed_count, we changes transition type to typed.
+            let transType = PlacesUtils.history.TRANSITION_LINK;
+            if (row.getResultByName("typed_count") > 0)
+              transType = PlacesUtils.history.TRANSITION_TYPED;
 
-          try {
-            PlacesUtils.asyncHistory.updatePlaces(places);
+            places.push({
+              uri: NetUtil.newURI(row.getResultByName("url")),
+              title: row.getResultByName("title"),
+              visits: [{
+                transitionType: transType,
+                visitDate: chromeTimeToDate(
+                             row.getResultByName(
+                               "last_visit_time")) * 1000,
+              }],
+            });
           } catch (e) {
             Cu.reportError(e);
           }
-        },
-
-        handleError : function(aError) {
-          Cu.reportError("Async statement execution returned with '" +
-                         aError.result + "', '" + aError.message + "'");
-        },
+        }
 
-        handleCompletion : function(aReason) {
-          dbConn.asyncClose();
-          aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
+        if (places.length > 0) {
+          yield new Promise((resolve, reject) => {
+            PlacesUtils.asyncHistory.updatePlaces(places, {
+              _success: false,
+              handleResult: function() {
+                // Importing any entry is considered a successful import.
+                this._success = true;
+              },
+              handleError: function() {},
+              handleCompletion: function() {
+                if (this._success) {
+                  resolve();
+                } else {
+                  reject(new Error("Couldn't add visits"));
+                }
+              }
+            });
+          });
         }
-      });
-      stmt.finalize();
+      }).then(() => { aCallback(true); },
+              ex => {
+                Cu.reportError(ex);
+                aCallback(false);
+              });
     }
   };
 }
 
 function GetCookiesResource(aProfileFolder) {
   let cookiesFile = aProfileFolder.clone();
   cookiesFile.append("Cookies");
   if (!cookiesFile.exists())
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -14,16 +14,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
                                   "resource://gre/modules/BookmarkHTMLUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+                                  "resource://gre/modules/PromiseUtils.jsm");
 
 var gMigrators = null;
 var gProfileStartup = null;
 var gMigrationBundle = null;
 
 XPCOMUtils.defineLazyGetter(this, "gAvailableMigratorKeys", function() {
   if (AppConstants.platform == "win") {
     return [
@@ -222,75 +224,88 @@ this.MigratorPrototype = {
   migrate: function MP_migrate(aItems, aStartup, aProfile) {
     let resources = this._getMaybeCachedResources(aProfile);
     if (resources.length == 0)
       throw new Error("migrate called for a non-existent source");
 
     if (aItems != Ci.nsIBrowserProfileMigrator.ALL)
       resources = resources.filter(r => aItems & r.type);
 
+    // Used to periodically give back control to the main-thread loop.
+    let unblockMainThread = function () {
+      return new Promise(resolve => {
+        Services.tm.mainThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+      });
+    };
+
     // Called either directly or through the bookmarks import callback.
-    function doMigrate() {
-      // TODO: use Map (for the items) and Set (for the resources)
-      // once they are iterable.
+    let doMigrate = Task.async(function*() {
       let resourcesGroupedByItems = new Map();
       resources.forEach(function(resource) {
-        if (resourcesGroupedByItems.has(resource.type))
-          resourcesGroupedByItems.get(resource.type).push(resource);
-        else
-          resourcesGroupedByItems.set(resource.type, [resource]);
+        if (!resourcesGroupedByItems.has(resource.type)) {
+          resourcesGroupedByItems.set(resource.type, new Set());
+        }
+        resourcesGroupedByItems.get(resource.type).add(resource)
       });
 
       if (resourcesGroupedByItems.size == 0)
         throw new Error("No items to import");
 
       let notify = function(aMsg, aItemType) {
         Services.obs.notifyObservers(null, aMsg, aItemType);
       }
 
       notify("Migration:Started");
       for (let [key, value] of resourcesGroupedByItems) {
-        // TODO: (bug 449811).
+        // Workaround bug 449811.
         let migrationType = key, itemResources = value;
 
         notify("Migration:ItemBeforeMigrate", migrationType);
 
         let itemSuccess = false;
         for (let res of itemResources) {
+          // Workaround bug 449811.
           let resource = res;
+          let completeDeferred = PromiseUtils.defer();
           let resourceDone = function(aSuccess) {
-            let resourceIndex = itemResources.indexOf(resource);
-            if (resourceIndex != -1) {
-              itemResources.splice(resourceIndex, 1);
-              itemSuccess |= aSuccess;
-              if (itemResources.length == 0) {
-                resourcesGroupedByItems.delete(migrationType);
-                notify(itemSuccess ?
-                       "Migration:ItemAfterMigrate" : "Migration:ItemError",
-                       migrationType);
-                if (resourcesGroupedByItems.size == 0)
-                  notify("Migration:Ended");
+            itemResources.delete(resource);
+            itemSuccess |= aSuccess;
+            if (itemResources.size == 0) {
+              notify(itemSuccess ?
+                     "Migration:ItemAfterMigrate" : "Migration:ItemError",
+                     migrationType);
+              resourcesGroupedByItems.delete(migrationType);
+              if (resourcesGroupedByItems.size == 0) {
+                notify("Migration:Ended");
               }
             }
+            completeDeferred.resolve();
           }
 
-          Services.tm.mainThread.dispatch(function() {
-            // If migrate throws, an error occurred, and the callback
-            // (itemMayBeDone) might haven't been called.
-            try {
-              resource.migrate(resourceDone);
-            }
-            catch(ex) {
-              Cu.reportError(ex);
-              resourceDone(false);
-            }
-          }, Ci.nsIThread.DISPATCH_NORMAL);
+          // If migrate throws, an error occurred, and the callback
+          // (itemMayBeDone) might haven't been called.
+          try {
+            resource.migrate(resourceDone);
+          }
+          catch(ex) {
+            Cu.reportError(ex);
+            resourceDone(false);
+          }
+
+          // Certain resources must be ran sequentially or they could fail,
+          // for example bookmarks and history (See bug 1272652).
+          if (migrationType == MigrationUtils.resourceTypes.BOOKMARKS ||
+              migrationType == MigrationUtils.resourceTypes.HISTORY) {
+            yield completeDeferred.promise;
+          }
+
+          yield unblockMainThread();
         }
       }
-    }
+    });
 
     if (MigrationUtils.isStartupMigration && !this.startupOnlyMigrator) {
       MigrationUtils.profileStartup.doStartup();
 
       // If we're about to migrate bookmarks, first import the default bookmarks.
       // Note We do not need to do so for the Firefox migrator
       // (=startupOnlyMigrator), as it just copies over the places database
       // from another profile.
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py
@@ -0,0 +1,414 @@
+import os, shutil
+from marionette import MarionetteTestCase
+
+
+class TestFirefoxRefresh(MarionetteTestCase):
+    _username = "marionette-test-login"
+    _password = "marionette-test-password"
+    _bookmarkURL = "about:mozilla"
+    _bookmarkText = "Some bookmark from Marionette"
+
+    _cookieHost = "firefox-refresh.marionette-test.mozilla.org"
+    _cookiePath = "some/cookie/path"
+    _cookieName = "somecookie"
+    _cookieValue = "some cookie value"
+
+    _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
+    _historyTitle = "Test visit for Firefox Reset"
+
+    _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
+    _formHistoryValue = "special-pumpkin-value"
+
+    _expectedURLs = ["about:robots", "about:mozilla"]
+
+    def savePassword(self):
+        self.runCode("""
+          let myLogin = new global.LoginInfo(
+            "test.marionette.mozilla.com",
+            "http://test.marionette.mozilla.com/some/form/",
+            null,
+            arguments[0],
+            arguments[1],
+            "username",
+            "password"
+          );
+          Services.logins.addLogin(myLogin)
+        """, script_args=[self._username, self._password])
+
+    def createBookmark(self):
+        self.marionette.execute_script("""
+          let url = arguments[0];
+          let title = arguments[1];
+          PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder,
+            makeURI(url), 0, title);
+        """, script_args=[self._bookmarkURL, self._bookmarkText])
+
+    def createHistory(self):
+        error = self.runAsyncCode("""
+          // Copied from PlacesTestUtils, which isn't available in Marionette tests.
+          let didReturn;
+          PlacesUtils.asyncHistory.updatePlaces(
+            [{title: arguments[1], uri: makeURI(arguments[0]), visits: [{
+                transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+                visitDate: (Date.now() - 5000) * 1000,
+                referrerURI: makeURI("about:mozilla"),
+              }]
+            }],
+            {
+              handleError(resultCode, place) {
+                didReturn = true;
+                marionetteScriptFinished("Unexpected error in adding visit: " + resultCode);
+              },
+              handleResult() {},
+              handleCompletion() {
+                if (!didReturn) {
+                  marionetteScriptFinished(false);
+                }
+              },
+            }
+          );
+        """, script_args=[self._historyURL, self._historyTitle])
+        if error:
+            print error
+
+    def createFormHistory(self):
+        error = self.runAsyncCode("""
+          let updateDefinition = {
+            op: "add",
+            fieldname: arguments[0],
+            value: arguments[1],
+            firstUsed: (Date.now() - 5000) * 1000,
+          };
+          let finished = false;
+          global.FormHistory.update(updateDefinition, {
+            handleError(error) {
+              finished = true;
+              marionetteScriptFinished(error);
+            },
+            handleCompletion() {
+              if (!finished) {
+                marionetteScriptFinished(false);
+              }
+            }
+          });
+        """, script_args=[self._formHistoryFieldName, self._formHistoryValue])
+        if error:
+          print error
+
+    def createCookie(self):
+        self.runCode("""
+          // Expire in 15 minutes:
+          let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
+          Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
+                               true, false, false, expireTime);
+        """, script_args=[self._cookieHost, self._cookiePath, self._cookieName, self._cookieValue])
+
+    def createSession(self):
+        self.runAsyncCode("""
+          const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
+                                 Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+          let expectedURLs = Array.from(arguments[0])
+          gBrowser.addTabsProgressListener({
+            onStateChange(browser, webprogress, request, flags, status) {
+              try {
+                request && request.QueryInterface(Ci.nsIChannel);
+              } catch (ex) {}
+              let uriLoaded = request.originalURI && request.originalURI.spec;
+              if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
+                  expectedURLs.includes(uriLoaded)) {
+                TabStateFlusher.flush(browser).then(function() {
+                  expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
+                  if (!expectedURLs.length) {
+                    gBrowser.removeTabsProgressListener(this);
+                    marionetteScriptFinished();
+                  }
+                });
+              }
+            }
+          });
+          for (let url of expectedURLs) {
+            gBrowser.addTab(url);
+          }
+        """, script_args=[self._expectedURLs])
+
+    def checkPassword(self):
+        loginInfo = self.marionette.execute_script("""
+          let ary = Services.logins.findLogins({},
+            "test.marionette.mozilla.com",
+            "http://test.marionette.mozilla.com/some/form/",
+            null, {});
+          return ary.length ? ary : {username: "null", password: "null"};
+        """)
+        self.assertEqual(len(loginInfo), 1)
+        self.assertEqual(loginInfo[0]['username'], self._username)
+        self.assertEqual(loginInfo[0]['password'], self._password)
+
+        loginCount = self.marionette.execute_script("""
+          return Services.logins.getAllLogins().length;
+        """)
+        self.assertEqual(loginCount, 1, "No other logins are present")
+
+    def checkBookmark(self):
+        titleInBookmarks = self.marionette.execute_script("""
+          let url = arguments[0];
+          let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(makeURI(url), {}, {});
+          return bookmarkIds.length == 1 ? PlacesUtils.bookmarks.getItemTitle(bookmarkIds[0]) : "";
+        """, script_args=[self._bookmarkURL])
+        self.assertEqual(titleInBookmarks, self._bookmarkText)
+
+    def checkHistory(self):
+        historyResults = self.runAsyncCode("""
+          let placeInfos = [];
+          PlacesUtils.asyncHistory.getPlacesInfo(makeURI(arguments[0]), {
+            handleError(resultCode, place) {
+              placeInfos = null;
+              marionetteScriptFinished("Unexpected error in fetching visit: " + resultCode);
+            },
+            handleResult(placeInfo) {
+              placeInfos.push(placeInfo);
+            },
+            handleCompletion() {
+              if (placeInfos) {
+                if (!placeInfos.length) {
+                  marionetteScriptFinished("No visits found");
+                } else {
+                  marionetteScriptFinished(placeInfos);
+                }
+              }
+            },
+          });
+        """, script_args=[self._historyURL])
+        if type(historyResults) == str:
+            self.fail(historyResults)
+            return
+
+        historyCount = len(historyResults)
+        self.assertEqual(historyCount, 1, "Should have exactly 1 entry for URI, got %d" % historyCount)
+        if historyCount == 1:
+            self.assertEqual(historyResults[0]['title'], self._historyTitle)
+
+    def checkFormHistory(self):
+        formFieldResults = self.runAsyncCode("""
+          let results = [];
+          global.FormHistory.search(["value"], {fieldname: arguments[0]}, {
+            handleError(error) {
+              results = error;
+            },
+            handleResult(result) {
+              results.push(result);
+            },
+            handleCompletion() {
+              marionetteScriptFinished(results);
+            },
+          });
+        """, script_args=[self._formHistoryFieldName])
+        if type(formFieldResults) == str:
+            self.fail(formFieldResults)
+            return
+
+        formFieldResultCount = len(formFieldResults)
+        self.assertEqual(formFieldResultCount, 1, "Should have exactly 1 entry for this field, got %d" % formFieldResultCount)
+        if formFieldResultCount == 1:
+            self.assertEqual(formFieldResults[0]['value'], self._formHistoryValue)
+
+        formHistoryCount = self.runAsyncCode("""
+          let count;
+          let callbacks = {
+            handleResult: rv => count = rv,
+            handleCompletion() {
+              marionetteScriptFinished(count);
+            },
+          };
+          global.FormHistory.count({}, callbacks);
+        """)
+        self.assertEqual(formHistoryCount, 1, "There should be only 1 entry in the form history")
+
+    def checkCookie(self):
+        cookieInfo = self.runCode("""
+          try {
+            let cookieEnum = Services.cookies.getCookiesFromHost(arguments[0]);
+            let cookie = null;
+            while (cookieEnum.hasMoreElements()) {
+              let hostCookie = cookieEnum.getNext();
+              hostCookie.QueryInterface(Ci.nsICookie2);
+              // getCookiesFromHost returns any cookie from the BASE host.
+              if (hostCookie.rawHost != arguments[0])
+                continue;
+              if (cookie != null) {
+                return "more than 1 cookie! That shouldn't happen!";
+              }
+              cookie = hostCookie;
+            }
+            return {path: cookie.path, name: cookie.name, value: cookie.value};
+          } catch (ex) {
+            return "got exception trying to fetch cookie: " + ex;
+          }
+        """, script_args=[self._cookieHost])
+        if not isinstance(cookieInfo, dict):
+            self.fail(cookieInfo)
+            return
+        self.assertEqual(cookieInfo['path'], self._cookiePath)
+        self.assertEqual(cookieInfo['value'], self._cookieValue)
+        self.assertEqual(cookieInfo['name'], self._cookieName)
+
+    def checkSession(self):
+        tabURIs = self.runCode("""
+          return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
+        """)
+        self.assertSequenceEqual(tabURIs, ["about:welcomeback"])
+
+        tabURIs = self.runAsyncCode("""
+          let mm = gBrowser.selectedBrowser.messageManager;
+          let fs = function() {
+            content.document.getElementById("errorTryAgain").click();
+          };
+          let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+          window.addEventListener("SSWindowStateReady", function testSSPostReset() {
+            window.removeEventListener("SSWindowStateReady", testSSPostReset, false);
+            Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() {
+              marionetteScriptFinished([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec));
+            });
+          }, false);
+          mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
+        """)
+        self.assertSequenceEqual(tabURIs, ["about:blank"] + self._expectedURLs)
+        pass
+
+    def checkProfile(self, hasMigrated=False):
+        self.checkPassword()
+        self.checkBookmark()
+        self.checkHistory()
+        self.checkFormHistory()
+        self.checkCookie()
+        if hasMigrated:
+            self.checkSession()
+
+    def createProfileData(self):
+        self.savePassword()
+        self.createBookmark()
+        self.createHistory()
+        self.createFormHistory()
+        self.createCookie()
+        self.createSession()
+
+    def setUpScriptData(self):
+        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+        self.marionette.execute_script("""
+          global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+          global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
+          global.Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+          global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory;
+        """, new_sandbox=False, sandbox='system')
+
+    def runCode(self, script, *args, **kwargs):
+        return self.marionette.execute_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
+
+    def runAsyncCode(self, script, *args, **kwargs):
+        return self.marionette.execute_async_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
+
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        self.setUpScriptData()
+
+        self.reset_profile_path = None
+        self.desktop_backup_path = None
+
+        self.createProfileData()
+
+    def tearDown(self):
+        # Force yet another restart with a clean profile to disconnect from the
+        # profile and environment changes we've made, to leave a more or less
+        # blank slate for the next person.
+        self.marionette.restart(clean=True, in_app=False)
+        self.setUpScriptData()
+
+        # Super
+        MarionetteTestCase.tearDown(self)
+
+        # Some helpers to deal with removing a load of files
+        import errno, stat
+        def handleRemoveReadonly(func, path, exc):
+            excvalue = exc[1]
+            if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
+                os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
+                func(path)
+            else:
+                raise
+
+        if self.desktop_backup_path:
+            shutil.rmtree(self.desktop_backup_path, ignore_errors=False, onerror=handleRemoveReadonly)
+
+        if self.reset_profile_path:
+            # Remove ourselves from profiles.ini
+            profileLeafName = os.path.basename(os.path.normpath(self.reset_profile_path))
+            self.runCode("""
+              let [salt, name] = arguments[0].split(".");
+              let profile = global.profSvc.getProfileByName(name);
+              profile.remove(false)
+              global.profSvc.flush();
+            """, script_args=[profileLeafName])
+            # And delete all the files.
+            shutil.rmtree(self.reset_profile_path, ignore_errors=False, onerror=handleRemoveReadonly)
+
+    def doReset(self):
+        self.runCode("""
+          // Ensure the current (temporary) profile is in profiles.ini:
+          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+          let profileName = "marionette-test-profile-" + Date.now();
+          let myProfile = global.profSvc.createProfile(profD, profileName);
+          global.profSvc.flush()
+
+          // Now add the reset parameters:
+          let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+          let allMarionettePrefs = Services.prefs.getChildList("marionette.");
+          let prefObj = {};
+          for (let pref of allMarionettePrefs) {
+            let prefSuffix = pref.substr("marionette.".length);
+            let prefVal = global.Preferences.get(pref);
+            prefObj[prefSuffix] = prefVal;
+          }
+          let marionetteInfo = JSON.stringify(prefObj);
+          env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", marionetteInfo);
+          env.set("MOZ_RESET_PROFILE_RESTART", "1");
+          env.set("XRE_PROFILE_PATH", arguments[0]);
+          env.set("XRE_PROFILE_NAME", profileName);
+        """, script_args=[self.marionette.instance.profile.profile])
+
+        profileLeafName = os.path.basename(os.path.normpath(self.marionette.instance.profile.profile))
+
+        # Now restart the browser to get it reset:
+        self.marionette.restart(clean=False, in_app=True)
+        self.setUpScriptData()
+
+        # Determine the new profile path (we'll need to remove it when we're done)
+        self.reset_profile_path = self.runCode("""
+          let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+          return profD.path;
+        """)
+
+        # Determine the backup path
+        self.desktop_backup_path = self.runCode("""
+          let container;
+          try {
+            container = Services.dirsvc.get("Desk", Ci.nsIFile);
+          } catch (ex) {
+            container = Services.dirsvc.get("Home", Ci.nsIFile);
+          }
+          let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
+          let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name], 1);
+          container.append(dirName);
+          container.append(arguments[0]);
+          return container.path;
+        """, script_args = [profileLeafName])
+
+        self.assertTrue(os.path.isdir(self.reset_profile_path), "Reset profile path should be present")
+        self.assertTrue(os.path.isdir(self.desktop_backup_path), "Backup profile path should be present")
+
+    def testReset(self):
+        self.checkProfile()
+
+        self.doReset()
+
+        # Now check that we're doing OK...
+        self.checkProfile(hasMigrated=True)
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -6,17 +6,17 @@
 
 this.EXPORTED_SYMBOLS = [
   "Sqlite",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 // The time to wait before considering a transaction stuck and rejecting it.
-const TRANSACTIONS_QUEUE_TIMEOUT_MS = 120000 // 2 minutes
+const TRANSACTIONS_QUEUE_TIMEOUT_MS = 240000 // 4 minutes
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");