commit 75ff40df4021290701c453be1eaaf120637d04b6
parent 35362df915a1b7c7cbca9ff65d73f473e548d2ae
Author: Nicholas Rishel <nrishel@mozilla.com>
Date: Mon, 3 Nov 2025 22:45:36 +0000
Bug 1993688 - Flush tab state before getting setting store state to backup. r=mconley,sthompson
Differential Revision: https://phabricator.services.mozilla.com/D270072
Diffstat:
3 files changed, 77 insertions(+), 0 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -3464,6 +3464,9 @@ pref("browser.backup.disabled-on-idle-backup-retry", false);
// removing for any reason.
pref("browser.backup.max-num-unremovable-staging-items", 5);
pref("browser.backup.scheduled.user-disabled", false);
+// How many milliseconds to wait for tab state to flush before continuing the
+// backup process.
+pref("browser.backup.tab-flush-timeout", 5000)
#ifdef NIGHTLY_BUILD
// Pref to enable the new profiles
diff --git a/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs
@@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
import {
BackupResource,
bytesToFuzzyKilobytes,
@@ -10,9 +12,28 @@ import {
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
+ return console.createInstance({
+ prefix: "SessionStoreBackupResource",
+ maxLogLevel: Services.prefs.getBoolPref("browser.backup.log", false)
+ ? "Debug"
+ : "Warn",
+ });
});
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "TAB_FLUSH_TIMEOUT",
+ "browser.backup.tab-flush-timeout",
+ 5000
+);
+
/**
* Class representing Session store related files within a user profile.
*/
@@ -44,6 +65,27 @@ export class SessionStoreBackupResource extends BackupResource {
profilePath = PathUtils.profileDir,
_isEncrypting = false
) {
+ // Flush tab state so backups receive the correct url to restore.
+ await Promise.race([
+ Promise.allSettled(
+ lazy.BrowserWindowTracker.orderedWindows.map(
+ lazy.TabStateFlusher.flushWindow
+ )
+ ),
+ new Promise((_, reject) =>
+ lazy.setTimeout(reject, lazy.TAB_FLUSH_TIMEOUT, { timeout: true })
+ ),
+ ]).catch(e => {
+ if (e?.timeout) {
+ lazy.logConsole.warn("Timed out waiting while flushing tab state.");
+ } else {
+ lazy.logConsole.error(
+ "Unrecognized error while flushing tab state.",
+ e
+ );
+ }
+ });
+
let sessionStoreState = this.#sessionStore.getCurrentState(true);
let sessionStorePath = PathUtils.join(stagingPath, "sessionstore.jsonlz4");
diff --git a/browser/components/backup/tests/marionette/test_backup.py b/browser/components/backup/tests/marionette/test_backup.py
@@ -27,6 +27,9 @@ class BackupTest(MarionetteTestCase):
"browser.backup.log": True,
"browser.backup.archive.enabled": True,
"browser.backup.restore.enabled": True,
+ # Necessary to test Session Restore from backup, which relies on
+ # the crash restore mechanism.
+ "browser.sessionstore.resume_from_crash": True,
}
)
@@ -94,6 +97,11 @@ class BackupTest(MarionetteTestCase):
# to be flushed to disk and to be made ready for backup
self.marionette.restart()
+ # We want to validate that TabState is flushed before serializing the
+ # backup, so run this test in the same browser instance we invoke the
+ # backup in.
+ self.add_test_sessionstore()
+
# Put the OSKeyStore label back, since it would have been cleared
# from memory during the restart.
self.marionette.execute_script(
@@ -254,6 +262,7 @@ class BackupTest(MarionetteTestCase):
self.verify_recovered_preferences()
self.verify_recovered_permissions()
self.verify_recovered_payment_methods(osKeyStoreLabel)
+ self.verify_recovered_sessionstore()
# Clean up the temporary OSKeyStore label
self.marionette.execute_async_script(
@@ -859,3 +868,26 @@ class BackupTest(MarionetteTestCase):
script_args=[osKeyStoreLabel],
)
self.assertTrue(cardExists)
+
+ def add_test_sessionstore(self):
+ with self.marionette.using_context("content"):
+ self.marionette.navigate("about:mozilla")
+
+ def verify_recovered_sessionstore(self):
+ [tabCount, url] = self.marionette.execute_script(
+ """
+ const { SessionStore } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionStore.sys.mjs"
+ );
+ const session = SessionStore.getCurrentState(true);
+ const win = session.windows[0];
+ const tabLen = win.tabs.length;
+ const tab = win.tabs[0];
+ const entry = tab.entries[0];
+ const url = entry.url;
+ return [tabLen, url];
+ """
+ )
+
+ self.assertEqual(tabCount, 1)
+ self.assertEqual(url, "about:mozilla")