summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/sessionstore')
-rw-r--r--browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs6
-rw-r--r--browser/components/sessionstore/SessionFile.sys.mjs20
-rw-r--r--browser/components/sessionstore/SessionLogger.sys.mjs87
-rw-r--r--browser/components/sessionstore/SessionStartup.sys.mjs68
-rw-r--r--browser/components/sessionstore/SessionStore.sys.mjs286
-rw-r--r--browser/components/sessionstore/SessionStoreFunctions.sys.mjs93
-rw-r--r--browser/components/sessionstore/components.conf12
-rw-r--r--browser/components/sessionstore/moz.build6
-rw-r--r--browser/components/sessionstore/test/marionette/manifest.toml4
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_sidebar.py110
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py110
11 files changed, 617 insertions, 185 deletions
diff --git a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
index d0627180f0..ebb2a66a53 100644
--- a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
+++ b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
@@ -183,9 +183,9 @@ export var RecentlyClosedTabsAndWindowsMenuUtils = {
* The command event when the user clicks the restore all menu item
*/
onRestoreAllWindowsCommand() {
- const count = lazy.SessionStore.getClosedWindowCount();
- for (let index = 0; index < count; index++) {
- lazy.SessionStore.undoCloseWindow(index);
+ const closedData = lazy.SessionStore.getClosedWindowData();
+ for (const { closedId } of closedData) {
+ lazy.SessionStore.undoCloseById(closedId);
}
},
diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
index 077529d739..a44a662e07 100644
--- a/browser/components/sessionstore/SessionFile.sys.mjs
+++ b/browser/components/sessionstore/SessionFile.sys.mjs
@@ -18,6 +18,7 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs",
@@ -234,7 +235,7 @@ var SessionFileInternal = {
// 1546847. Just in case there are problems in the format of
// the parsed data, continue on. Favicons might be broken, but
// the session will at least be recovered
- console.error(e);
+ lazy.sessionStoreLogger.error(e);
}
}
@@ -247,7 +248,7 @@ var SessionFileInternal = {
)
) {
// Skip sessionstore files that we don't understand.
- console.error(
+ lazy.sessionStoreLogger.warn(
"Cannot extract data from Session Restore file ",
path,
". Wrong format/version: " + JSON.stringify(parsed.version) + "."
@@ -289,6 +290,7 @@ var SessionFileInternal = {
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
.add(Date.now() - startMs);
+ lazy.sessionStoreLogger.debug(`Successful file read of ${key} file`);
break;
} catch (ex) {
if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
@@ -304,13 +306,20 @@ var SessionFileInternal = {
loadfail_reason: "File doesn't exist.",
}
);
+ // A file not existing can be normal and expected.
+ lazy.sessionStoreLogger.debug(
+ `Can't read session file which doesn't exist: ${key}`
+ );
} else if (
DOMException.isInstance(ex) &&
ex.name == "NotAllowedError"
) {
// The file might be inaccessible due to wrong permissions
// or similar failures. We'll just count it as "corrupted".
- console.error("Could not read session file ", ex);
+ lazy.sessionStoreLogger.error(
+ `NotAllowedError when reading session file: ${key}`,
+ ex
+ );
corrupted = true;
Services.telemetry.recordEvent(
"session_restore",
@@ -324,7 +333,7 @@ var SessionFileInternal = {
}
);
} else if (ex instanceof SyntaxError) {
- console.error(
+ lazy.sessionStoreLogger.error(
"Corrupt session file (invalid JSON found) ",
ex,
ex.stack
@@ -385,6 +394,9 @@ var SessionFileInternal = {
if (!result) {
// If everything fails, start with an empty session.
+ lazy.sessionStoreLogger.warn(
+ "No readable session files found to restore, starting with empty session"
+ );
result = {
origin: "empty",
source: "",
diff --git a/browser/components/sessionstore/SessionLogger.sys.mjs b/browser/components/sessionstore/SessionLogger.sys.mjs
new file mode 100644
index 0000000000..a7c99f911e
--- /dev/null
+++ b/browser/components/sessionstore/SessionLogger.sys.mjs
@@ -0,0 +1,87 @@
+/* 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/. */
+
+import { LogManager } from "resource://gre/modules/LogManager.sys.mjs";
+// See Bug 1889052
+// eslint-disable-next-line mozilla/use-console-createInstance
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ cancelIdleCallback: "resource://gre/modules/Timer.sys.mjs",
+ requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const loggerNames = ["SessionStore"];
+
+export const sessionStoreLogger = Log.repository.getLogger("SessionStore");
+sessionStoreLogger.manageLevelFromPref("browser.sessionstore.loglevel");
+
+class SessionLogManager extends LogManager {
+ #idleCallbackId = null;
+ #observers = new Set();
+
+ QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]);
+
+ constructor(options = {}) {
+ super(options);
+
+ Services.obs.addObserver(this, "sessionstore-windows-restored");
+ this.#observers.add("sessionstore-windows-restored");
+
+ lazy.AsyncShutdown.profileBeforeChange.addBlocker(
+ "SessionLogManager: finalize and flush any logs to disk",
+ () => {
+ return this.stop();
+ }
+ );
+ }
+
+ async stop() {
+ if (this.#observers.has("sessionstore-windows-restored")) {
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this.#observers.delete("sessionstore-windows-restored");
+ }
+ await this.requestLogFlush(true);
+ this.finalize();
+ }
+
+ observe(subject, topic, _) {
+ switch (topic) {
+ case "sessionstore-windows-restored":
+ // this represents the moment session restore is nominally complete
+ // and is a good time to ensure any log messages are flushed to disk
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this.#observers.delete("sessionstore-windows-restored");
+ this.requestLogFlush();
+ break;
+ }
+ }
+
+ async requestLogFlush(immediate = false) {
+ if (this.#idleCallbackId && !immediate) {
+ return;
+ }
+ if (this.#idleCallbackId) {
+ lazy.cancelIdleCallback(this.#idleCallbackId);
+ this.#idleCallbackId = null;
+ }
+ if (!immediate) {
+ await new Promise(resolve => {
+ this.#idleCallbackId = lazy.requestIdleCallback(resolve);
+ });
+ this.#idleCallbackId = null;
+ }
+ await this.resetFileLog();
+ }
+}
+
+export const logManager = new SessionLogManager({
+ prefRoot: "browser.sessionstore.",
+ logNames: loggerNames,
+ logFilePrefix: "sessionrestore",
+ logFileSubDirectoryEntries: ["sessionstore-logs"],
+ testTopicPrefix: "sessionrestore:log-manager:",
+});
diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs
index 0d017ac035..72df4316e9 100644
--- a/browser/components/sessionstore/SessionStartup.sys.mjs
+++ b/browser/components/sessionstore/SessionStartup.sys.mjs
@@ -38,6 +38,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
StartupPerformance:
"resource:///modules/sessionstore/StartupPerformance.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
});
const STATE_RUNNING_STR = "running";
@@ -50,32 +51,7 @@ const TYPE_DEFER_SESSION = 3;
// 'browser.startup.page' preference value to resume the previous session.
const BROWSER_STARTUP_RESUME_SESSION = 3;
-function warning(msg, exception) {
- let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
- Ci.nsIScriptError
- );
- consoleMsg.init(
- msg,
- exception.fileName,
- null,
- exception.lineNumber,
- 0,
- Ci.nsIScriptError.warningFlag,
- "component javascript"
- );
- Services.console.logMessage(consoleMsg);
-}
-
-var gOnceInitializedDeferred = (function () {
- let deferred = {};
-
- deferred.promise = new Promise((resolve, reject) => {
- deferred.resolve = resolve;
- deferred.reject = reject;
- });
-
- return deferred;
-})();
+var gOnceInitializedDeferred = Promise.withResolvers();
/* :::::::: The Service ::::::::::::::: */
@@ -119,6 +95,7 @@ export var SessionStartup = {
"browser.sessionstore.resuming_after_os_restart"
)
) {
+ lazy.sessionStoreLogger.debug("resuming_after_os_restart");
if (!Services.appinfo.restartedByOS) {
// We had set resume_session_once in order to resume after an OS restart,
// but we aren't automatically started by the OS (or else appinfo.restartedByOS
@@ -136,8 +113,17 @@ export var SessionStartup = {
}
lazy.SessionFile.read().then(
- this._onSessionFileRead.bind(this),
- console.error
+ result => {
+ lazy.sessionStoreLogger.debug(
+ `Completed SessionFile.read() with result.origin: ${result.origin}`
+ );
+ return this._onSessionFileRead(result);
+ },
+ err => {
+ // SessionFile.read catches most expected failures,
+ // so a promise rejection here should be logged as an error
+ lazy.sessionStoreLogger.error("Failure from _onSessionFileRead", err);
+ }
);
},
@@ -174,12 +160,18 @@ export var SessionStartup = {
if (stateString != source) {
// The session has been modified by an add-on, reparse.
+ lazy.sessionStoreLogger.debug(
+ "After sessionstore-state-read, session has been modified"
+ );
try {
this._initialState = JSON.parse(stateString);
} catch (ex) {
// That's not very good, an add-on has rewritten the initial
// state to something that won't parse.
- warning("Observer rewrote the state to something that won't parse", ex);
+ lazy.sessionStoreLogger.error(
+ "'sessionstore-state-read' observer rewrote the state to something that won't parse",
+ ex
+ );
}
} else {
// No need to reparse
@@ -189,6 +181,7 @@ export var SessionStartup = {
if (this._initialState == null) {
// No valid session found.
this._sessionType = this.NO_SESSION;
+ lazy.sessionStoreLogger.debug("No valid session found");
Services.obs.notifyObservers(null, "sessionstore-state-finalized");
gOnceInitializedDeferred.resolve();
return;
@@ -204,14 +197,24 @@ export var SessionStartup = {
}, 0)
);
}, 0);
+ lazy.sessionStoreLogger.debug(
+ `initialState contains ${pinnedTabCount} pinned tabs`
+ );
Services.telemetry.scalarSetMaximum(
"browser.engagement.max_concurrent_tab_pinned_count",
pinnedTabCount
);
}, 60000);
+ let isAutomaticRestoreEnabled = this.isAutomaticRestoreEnabled();
+ lazy.sessionStoreLogger.debug(
+ `isAutomaticRestoreEnabled: ${isAutomaticRestoreEnabled}`
+ );
// If this is a normal restore then throw away any previous session.
- if (!this.isAutomaticRestoreEnabled() && this._initialState) {
+ if (!isAutomaticRestoreEnabled && this._initialState) {
+ lazy.sessionStoreLogger.debug(
+ "Discarding previous session as we have initialState"
+ );
delete this._initialState.lastSessionState;
}
@@ -276,10 +279,14 @@ export var SessionStartup = {
shutdown_reason: previousSessionCrashedReason,
}
);
+ lazy.sessionStoreLogger.debug(
+ `Previous shutdown ok? ${this._previousSessionCrashed}, reason: ${previousSessionCrashedReason}`
+ );
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
if (this.sessionType == this.NO_SESSION) {
+ lazy.sessionStoreLogger.debug("Will restore no session");
this._initialState = null; // Reset the state.
} else {
Services.obs.addObserver(this, "browser:purge-session-history", true);
@@ -299,6 +306,7 @@ export var SessionStartup = {
switch (topic) {
case "sessionstore-windows-restored":
Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ lazy.sessionStoreLogger.debug(`sessionstore-windows-restored`);
// Free _initialState after nsSessionStore is done with it.
this._initialState = null;
this._didRestore = true;
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index 16137b8388..7bd262fe09 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -169,12 +169,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
HomePage: "resource:///modules/HomePage.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
SessionCookies: "resource:///modules/sessionstore/SessionCookies.sys.mjs",
SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
+ SessionStoreHelper:
+ "resource://gre/modules/sessionstore/SessionStoreHelper.sys.mjs",
TabAttributes: "resource:///modules/sessionstore/TabAttributes.sys.mjs",
TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
TabState: "resource:///modules/sessionstore/TabState.sys.mjs",
@@ -205,6 +208,9 @@ var gResistFingerprintingEnabled = false;
* @namespace SessionStore
*/
export var SessionStore = {
+ get logger() {
+ return SessionStoreInternal._log;
+ },
get promiseInitialized() {
return SessionStoreInternal.promiseInitialized;
},
@@ -1046,6 +1052,10 @@ var SessionStoreInternal = {
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_PRIVACY_LEVEL")
.add(Services.prefs.getIntPref("browser.sessionstore.privacy_level"));
+
+ this.promiseAllWindowsRestored.finally(() => () => {
+ this._log.debug("promiseAllWindowsRestored finalized");
+ });
},
/**
@@ -1055,10 +1065,13 @@ var SessionStoreInternal = {
TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
let state;
let ss = lazy.SessionStartup;
-
- if (ss.willRestore() || ss.sessionType == ss.DEFER_SESSION) {
+ let willRestore = ss.willRestore();
+ if (willRestore || ss.sessionType == ss.DEFER_SESSION) {
state = ss.state;
}
+ this._log.debug(
+ `initSession willRestore: ${willRestore}, SessionStartup.sessionType: ${ss.sessionType}`
+ );
if (state) {
try {
@@ -1074,6 +1087,9 @@ var SessionStoreInternal = {
} else {
state = null;
}
+ this._log.debug(
+ `initSession deferred restore with ${iniState.windows.length} initial windows, ${remainingState.windows.length} remaining windows`
+ );
if (remainingState.windows.length) {
LastSession.setState(remainingState);
@@ -1092,6 +1108,9 @@ var SessionStoreInternal = {
if (restoreAsCrashed) {
this._recentCrashes =
((state.session && state.session.recentCrashes) || 0) + 1;
+ this._log.debug(
+ `initSession, restoreAsCrashed, crashes: ${this._recentCrashes}`
+ );
// _needsRestorePage will record sessionrestore_interstitial,
// including the specific reason we decided we needed to show
@@ -1106,9 +1125,11 @@ var SessionStoreInternal = {
lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
};
state = { windows: [{ tabs: [{ entries: [entry], formdata }] }] };
+ this._log.debug("initSession, will show about:sessionrestore");
} else if (
this._hasSingleTabWithURL(state.windows, "about:welcomeback")
) {
+ this._log.debug("initSession, will show about:welcomeback");
Services.telemetry.keyedScalarAdd(
"browser.engagement.sessionrestore_interstitial",
"shown_only_about_welcomeback",
@@ -1131,7 +1152,7 @@ var SessionStoreInternal = {
"autorestore",
1
);
-
+ this._log.debug("initSession, will autorestore");
this._removeExplicitlyClosedTabs(state);
}
@@ -1159,7 +1180,7 @@ var SessionStoreInternal = {
state?.windows?.forEach(win => delete win._maybeDontRestoreTabs);
state?._closedWindows?.forEach(win => delete win._maybeDontRestoreTabs);
} catch (ex) {
- this._log.error("The session file is invalid: " + ex);
+ this._log.error("The session file is invalid: ", ex);
}
}
@@ -1243,10 +1264,7 @@ var SessionStoreInternal = {
gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
});
- this._log = console.createInstance({
- prefix: "SessionStore",
- maxLogLevel: gDebuggingEnabled ? "Debug" : "Warn",
- });
+ this._log = lazy.sessionStoreLogger;
this._max_tabs_undo = this._prefBranch.getIntPref(
"sessionstore.max_tabs_undo"
@@ -1363,16 +1381,11 @@ var SessionStoreInternal = {
}
break;
case "browsing-context-did-set-embedder":
- if (
- aSubject &&
- aSubject === aSubject.top &&
- aSubject.isContent &&
- aSubject.embedderElement &&
- aSubject.embedderElement.permanentKey
- ) {
- let permanentKey = aSubject.embedderElement.permanentKey;
- this._browserSHistoryListener.get(permanentKey)?.unregister();
- this.getOrCreateSHistoryListener(permanentKey, aSubject, true);
+ if (aSubject === aSubject.top && aSubject.isContent) {
+ const permanentKey = aSubject.embedderElement?.permanentKey;
+ if (permanentKey) {
+ this.maybeRecreateSHistoryListener(permanentKey, aSubject);
+ }
}
break;
case "browsing-context-discarded":
@@ -1388,11 +1401,28 @@ var SessionStoreInternal = {
}
},
- getOrCreateSHistoryListener(
- permanentKey,
- browsingContext,
- collectImmediately = false
- ) {
+ getOrCreateSHistoryListener(permanentKey, browsingContext) {
+ if (!permanentKey || browsingContext !== browsingContext.top) {
+ return null;
+ }
+
+ const listener = this._browserSHistoryListener.get(permanentKey);
+ if (listener) {
+ return listener;
+ }
+
+ return this.createSHistoryListener(permanentKey, browsingContext, false);
+ },
+
+ maybeRecreateSHistoryListener(permanentKey, browsingContext) {
+ const listener = this._browserSHistoryListener.get(permanentKey);
+ if (!listener || listener._browserId != browsingContext.browserId) {
+ listener?.unregister(permanentKey);
+ this.createSHistoryListener(permanentKey, browsingContext, true);
+ }
+ },
+
+ createSHistoryListener(permanentKey, browsingContext, collectImmediately) {
class SHistoryListener {
constructor() {
this.QueryInterface = ChromeUtils.generateQI([
@@ -1495,21 +1525,12 @@ var SessionStoreInternal = {
}
}
- if (!permanentKey || browsingContext !== browsingContext.top) {
- return null;
- }
-
let sessionHistory = browsingContext.sessionHistory;
if (!sessionHistory) {
return null;
}
- let listener = this._browserSHistoryListener.get(permanentKey);
- if (listener) {
- return listener;
- }
-
- listener = new SHistoryListener();
+ const listener = new SHistoryListener();
sessionHistory.addSHistoryListener(listener);
this._browserSHistoryListener.set(permanentKey, listener);
@@ -1804,6 +1825,9 @@ var SessionStoreInternal = {
lazy.SessionSaver.updateLastSaveTime();
if (isPrivateWindow) {
+ this._log.debug(
+ "initializeWindow, the window is private. Saving SessionStartup.state for possibly restoring later"
+ );
// We're starting with a single private window. Save the state we
// actually wanted to restore so that we can do it later in case
// the user opens another, non-private window.
@@ -1901,7 +1925,7 @@ var SessionStoreInternal = {
windows: [closedWindowState],
});
- // These are our pinned tabs, which we should restore
+ // These are our pinned tabs and sidebar attributes, which we should restore
if (appTabsState.windows.length) {
newWindowState = appTabsState.windows[0];
delete newWindowState.__lastSessionWindowID;
@@ -1961,6 +1985,9 @@ var SessionStoreInternal = {
// Just call initializeWindow() directly if we're initialized already.
if (this._sessionInitialized) {
+ this._log.debug(
+ "onBeforeBrowserWindowShown, session already initialized, initializing window"
+ );
this.initializeWindow(aWindow);
return;
}
@@ -1996,6 +2023,9 @@ var SessionStoreInternal = {
this._promiseReadyForInitialization
.then(() => {
if (aWindow.closed) {
+ this._log.debug(
+ "When _promiseReadyForInitialization resolved, the window was closed"
+ );
return;
}
@@ -2020,7 +2050,12 @@ var SessionStoreInternal = {
this._deferredInitialized.resolve();
}
})
- .catch(console.error);
+ .catch(ex => {
+ this._log.error(
+ "Exception when handling _promiseReadyForInitialization resolution:",
+ ex
+ );
+ });
},
/**
@@ -4526,12 +4561,16 @@ var SessionStoreInternal = {
}
let sidebarBox = aWindow.document.getElementById("sidebar-box");
- let sidebar = sidebarBox.getAttribute("sidebarcommand");
- if (sidebar && sidebarBox.getAttribute("checked") == "true") {
- winData.sidebar = sidebar;
- } else if (winData.sidebar) {
- delete winData.sidebar;
+ let command = sidebarBox.getAttribute("sidebarcommand");
+ if (command && sidebarBox.getAttribute("checked") == "true") {
+ winData.sidebar = {
+ command,
+ positionEnd: sidebarBox.getAttribute("positionend"),
+ };
+ } else if (winData.sidebar?.command) {
+ delete winData.sidebar.command;
}
+
let workspaceID = aWindow.getWorkspaceID();
if (workspaceID) {
winData.workspaceID = workspaceID;
@@ -4755,6 +4794,7 @@ var SessionStoreInternal = {
let windowsOpened = [];
for (let winData of root.windows) {
if (!winData || !winData.tabs || !winData.tabs[0]) {
+ this._log.debug(`_openWindows, skipping window with no tabs data`);
this._restoreCount--;
continue;
}
@@ -4799,6 +4839,8 @@ var SessionStoreInternal = {
let overwriteTabs = aOptions && aOptions.overwriteTabs;
let firstWindow = aOptions && aOptions.firstWindow;
+ this.restoreSidebar(aWindow, winData.sidebar);
+
// initialize window if necessary
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) {
this.onLoad(aWindow);
@@ -4812,6 +4854,7 @@ var SessionStoreInternal = {
this._setWindowStateBusy(aWindow);
if (winData.workspaceID) {
+ this._log.debug(`Moving window to workspace: ${winData.workspaceID}`);
aWindow.moveToWorkspace(winData.workspaceID);
}
@@ -4864,12 +4907,18 @@ var SessionStoreInternal = {
this._prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") &&
this._restore_on_demand;
+ this._log.debug(
+ `restoreWindow, will restore ${winData.tabs.length} tabs, restoreTabsLazily: ${restoreTabsLazily}`
+ );
if (winData.tabs.length) {
var tabs = tabbrowser.createTabsForSessionRestore(
restoreTabsLazily,
selectTab,
winData.tabs
);
+ this._log.debug(
+ `restoreWindow, createTabsForSessionRestore returned {tabs.length} tabs`
+ );
}
// Move the originally open tabs to the end.
@@ -5091,6 +5140,7 @@ var SessionStoreInternal = {
root = typeof aState == "string" ? JSON.parse(aState) : aState;
} catch (ex) {
// invalid state object - don't restore anything
+ this._log.debug(`restoreWindows failed to parse ${typeof aState} state`);
this._log.error(ex);
this._sendRestoreCompletedNotifications();
return;
@@ -5109,9 +5159,13 @@ var SessionStoreInternal = {
);
}
}
+ this._log.debug(`Restored ${this._closedWindows.length} closed windows`);
this._closedObjectsChanged = true;
}
+ this._log.debug(
+ `restoreWindows will restore ${root.windows?.length} windows`
+ );
// We're done here if there are no windows.
if (!root.windows || !root.windows.length) {
this._sendRestoreCompletedNotifications();
@@ -5234,7 +5288,7 @@ var SessionStoreInternal = {
let browser = tab.linkedBrowser;
if (TAB_STATE_FOR_BROWSER.has(browser)) {
- console.error("Must reset tab before calling restoreTab.");
+ this._log.warn("Must reset tab before calling restoreTab.");
return;
}
@@ -5549,13 +5603,34 @@ var SessionStoreInternal = {
"screenX" in aWinData ? +aWinData.screenX : NaN,
"screenY" in aWinData ? +aWinData.screenY : NaN,
aWinData.sizemode || "",
- aWinData.sizemodeBeforeMinimized || "",
- aWinData.sidebar || ""
+ aWinData.sizemodeBeforeMinimized || ""
);
+ this.restoreSidebar(aWindow, aWinData.sidebar);
}, 0);
},
/**
+ * @param aWindow
+ * Window reference
+ * @param aSidebar
+ * Object containing command (sidebarcommand/category) and
+ * positionEnd (reflecting the sidebar.position_start pref)
+ */
+ restoreSidebar(aWindow, aSidebar) {
+ let sidebarBox = aWindow.document.getElementById("sidebar-box");
+ if (
+ aSidebar?.command &&
+ (sidebarBox.getAttribute("sidebarcommand") != aSidebar.command ||
+ !sidebarBox.getAttribute("checked"))
+ ) {
+ aWindow.SidebarController.showInitially(aSidebar.command);
+ if (aSidebar?.positionEnd) {
+ sidebarBox.setAttribute("positionend", "");
+ }
+ }
+ },
+
+ /**
* Restore a window's dimensions
* @param aWidth
* Window width in desktop pixels
@@ -5569,8 +5644,6 @@ var SessionStoreInternal = {
* Window size mode (eg: maximized)
* @param aSizeModeBeforeMinimized
* Window size mode before window got minimized (eg: maximized)
- * @param aSidebar
- * Sidebar command
*/
restoreDimensions: function ssi_restoreDimensions(
aWindow,
@@ -5579,8 +5652,7 @@ var SessionStoreInternal = {
aLeft,
aTop,
aSizeMode,
- aSizeModeBeforeMinimized,
- aSidebar
+ aSizeModeBeforeMinimized
) {
var win = aWindow;
var _this = this;
@@ -5722,14 +5794,6 @@ var SessionStoreInternal = {
break;
}
}
- let sidebarBox = aWindow.document.getElementById("sidebar-box");
- if (
- aSidebar &&
- (sidebarBox.getAttribute("sidebarcommand") != aSidebar ||
- !sidebarBox.getAttribute("checked"))
- ) {
- aWindow.SidebarUI.showInitially(aSidebar);
- }
// since resizing/moving a window brings it to the foreground,
// we might want to re-focus the last focused window
if (this.windowToFocus) {
@@ -5972,6 +6036,11 @@ var SessionStoreInternal = {
features.push("private");
}
+ this._log.debug(
+ `Opening window with features: ${features.join(
+ ","
+ )}, argString: ${argString}.`
+ );
var window = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
@@ -6251,6 +6320,15 @@ var SessionStoreInternal = {
if (PERSIST_SESSIONS) {
newWindowState._closedTabs = Cu.cloneInto(window._closedTabs, {});
}
+
+ // We want to preserve the sidebar if previously open in the window
+ if (window.sidebar?.command) {
+ newWindowState.sidebar = {
+ command: window.sidebar.command,
+ positionEnd: !!window.sidebar.positionEnd,
+ };
+ }
+
for (let tIndex = 0; tIndex < window.tabs.length; ) {
if (window.tabs[tIndex].pinned) {
// Adjust window.selected
@@ -6383,6 +6461,7 @@ var SessionStoreInternal = {
// This was the last window restored at startup, notify observers.
if (!this._browserSetState) {
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
+ this._log.debug(`All ${this._restoreCount} windows restored`);
this._deferredAllWindowsRestored.resolve();
} else {
// _browserSetState is used only by tests, and it uses an alternate
@@ -6691,98 +6770,6 @@ var SessionStoreInternal = {
return deferred;
},
- /**
- * Builds a single nsISessionStoreRestoreData tree for the provided |formdata|
- * and |scroll| trees.
- */
- buildRestoreData(formdata, scroll) {
- function addFormEntries(root, fields, isXpath) {
- for (let [key, value] of Object.entries(fields)) {
- switch (typeof value) {
- case "string":
- root.addTextField(isXpath, key, value);
- break;
- case "boolean":
- root.addCheckbox(isXpath, key, value);
- break;
- case "object": {
- if (value === null) {
- break;
- }
- if (
- value.hasOwnProperty("type") &&
- value.hasOwnProperty("fileList")
- ) {
- root.addFileList(isXpath, key, value.type, value.fileList);
- break;
- }
- if (
- value.hasOwnProperty("selectedIndex") &&
- value.hasOwnProperty("value")
- ) {
- root.addSingleSelect(
- isXpath,
- key,
- value.selectedIndex,
- value.value
- );
- break;
- }
- if (
- value.hasOwnProperty("value") &&
- value.hasOwnProperty("state")
- ) {
- root.addCustomElement(isXpath, key, value.value, value.state);
- break;
- }
- if (
- key === "sessionData" &&
- ["about:sessionrestore", "about:welcomeback"].includes(
- formdata.url
- )
- ) {
- root.addTextField(isXpath, key, JSON.stringify(value));
- break;
- }
- if (Array.isArray(value)) {
- root.addMultipleSelect(isXpath, key, value);
- break;
- }
- }
- }
- }
- }
-
- let root = SessionStoreUtils.constructSessionStoreRestoreData();
- if (scroll?.hasOwnProperty("scroll")) {
- root.scroll = scroll.scroll;
- }
- if (formdata?.hasOwnProperty("url")) {
- root.url = formdata.url;
- if (formdata.hasOwnProperty("innerHTML")) {
- // eslint-disable-next-line no-unsanitized/property
- root.innerHTML = formdata.innerHTML;
- }
- if (formdata.hasOwnProperty("xpath")) {
- addFormEntries(root, formdata.xpath, /* isXpath */ true);
- }
- if (formdata.hasOwnProperty("id")) {
- addFormEntries(root, formdata.id, /* isXpath */ false);
- }
- }
- let childrenLength = Math.max(
- scroll?.children?.length || 0,
- formdata?.children?.length || 0
- );
- for (let i = 0; i < childrenLength; i++) {
- root.addChild(
- this.buildRestoreData(formdata?.children?.[i], scroll?.children?.[i]),
- i
- );
- }
- return root;
- },
-
_waitForStateStop(browser, expectedURL = null) {
const deferred = Promise.withResolvers();
@@ -6956,7 +6943,10 @@ var SessionStoreInternal = {
if (!haveUserTypedValue && tabData.entries.length) {
return SessionStoreUtils.initializeRestore(
browser.browsingContext,
- this.buildRestoreData(tabData.formdata, tabData.scroll)
+ lazy.SessionStoreHelper.buildRestoreData(
+ tabData.formdata,
+ tabData.scroll
+ )
);
}
// Here, we need to load user data or about:blank instead.
diff --git a/browser/components/sessionstore/SessionStoreFunctions.sys.mjs b/browser/components/sessionstore/SessionStoreFunctions.sys.mjs
new file mode 100644
index 0000000000..978c1a79cd
--- /dev/null
+++ b/browser/components/sessionstore/SessionStoreFunctions.sys.mjs
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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/. */
+import { SessionStore } from "resource:///modules/sessionstore/SessionStore.sys.mjs";
+
+export class SessionStoreFunctions {
+ UpdateSessionStore(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aCollectSHistory,
+ aData
+ ) {
+ return SessionStoreFuncInternal.updateSessionStore(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aCollectSHistory,
+ aData
+ );
+ }
+
+ UpdateSessionStoreForStorage(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aData
+ ) {
+ return SessionStoreFuncInternal.updateSessionStoreForStorage(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aData
+ );
+ }
+}
+
+var SessionStoreFuncInternal = {
+ updateSessionStore: function SSF_updateSessionStore(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aCollectSHistory,
+ aData
+ ) {
+ let { formdata, scroll } = aData;
+
+ if (formdata) {
+ aData.formdata = formdata.toJSON();
+ }
+
+ if (scroll) {
+ aData.scroll = scroll.toJSON();
+ }
+
+ SessionStore.updateSessionStoreFromTablistener(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ {
+ data: aData,
+ epoch: aEpoch,
+ sHistoryNeeded: aCollectSHistory,
+ }
+ );
+ },
+
+ updateSessionStoreForStorage: function SSF_updateSessionStoreForStorage(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aData
+ ) {
+ SessionStore.updateSessionStoreFromTablistener(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ { data: { storage: aData }, epoch: aEpoch },
+ true
+ );
+ },
+};
+
+SessionStoreFunctions.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsISessionStoreFunctions",
+]);
diff --git a/browser/components/sessionstore/components.conf b/browser/components/sessionstore/components.conf
new file mode 100644
index 0000000000..2776c4dcb7
--- /dev/null
+++ b/browser/components/sessionstore/components.conf
@@ -0,0 +1,12 @@
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{45ce6b2d-ffc8-4051-bb41-37ceeeb19e94}',
+ 'contract_ids': ['@mozilla.org/toolkit/sessionstore-functions;1'],
+ 'esModule': 'resource:///modules/sessionstore/SessionStoreFunctions.sys.mjs',
+ 'constructor': 'SessionStoreFunctions',
+ },
+]
diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build
index cd3a0ad6fc..97554aab31 100644
--- a/browser/components/sessionstore/moz.build
+++ b/browser/components/sessionstore/moz.build
@@ -16,10 +16,12 @@ EXTRA_JS_MODULES.sessionstore = [
"RunState.sys.mjs",
"SessionCookies.sys.mjs",
"SessionFile.sys.mjs",
+ "SessionLogger.sys.mjs",
"SessionMigration.sys.mjs",
"SessionSaver.sys.mjs",
"SessionStartup.sys.mjs",
"SessionStore.sys.mjs",
+ "SessionStoreFunctions.sys.mjs",
"SessionWriter.sys.mjs",
"StartupPerformance.sys.mjs",
"TabAttributes.sys.mjs",
@@ -28,6 +30,10 @@ EXTRA_JS_MODULES.sessionstore = [
"TabStateFlusher.sys.mjs",
]
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
TESTING_JS_MODULES += [
"test/SessionStoreTestUtils.sys.mjs",
]
diff --git a/browser/components/sessionstore/test/marionette/manifest.toml b/browser/components/sessionstore/test/marionette/manifest.toml
index 6b62bea84e..0ff186778a 100644
--- a/browser/components/sessionstore/test/marionette/manifest.toml
+++ b/browser/components/sessionstore/test/marionette/manifest.toml
@@ -9,6 +9,10 @@ tags = "local"
["test_restore_manually_with_pinned_tabs.py"]
+["test_restore_sidebar_automatic.py"]
+
+["test_restore_sidebar.py"]
+
["test_restore_windows_after_close_last_tabs.py"]
skip-if = ["os == 'mac'"]
diff --git a/browser/components/sessionstore/test/marionette/test_restore_sidebar.py b/browser/components/sessionstore/test/marionette/test_restore_sidebar.py
new file mode 100644
index 0000000000..042b9f2b23
--- /dev/null
+++ b/browser/components/sessionstore/test/marionette/test_restore_sidebar.py
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 0.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/0.0/.
+
+import os
+import sys
+
+# add this directory to the path
+sys.path.append(os.path.dirname(__file__))
+
+from session_store_test_case import SessionStoreTestCase
+
+
+def inline(title):
+ return "data:text/html;charset=utf-8,<html><head><title>{}</title></head><body></body></html>".format(
+ title
+ )
+
+
+class TestSessionRestore(SessionStoreTestCase):
+ """
+ Test that the sidebar and its attributes are restored on reopening of window.
+ """
+
+ def setUp(self):
+ super(TestSessionRestore, self).setUp(
+ startup_page=1,
+ include_private=False,
+ restore_on_demand=True,
+ test_windows=set(
+ [
+ (
+ inline("lorem ipsom"),
+ inline("dolor"),
+ ),
+ ]
+ ),
+ )
+
+ def test_restore(self):
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Should have 1 window open.",
+ )
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ window.SidebarController.show("viewHistorySidebar");
+ let sidebarBox = window.document.getElementById("sidebar-box")
+ sidebarBox.style.width = "100px";
+ """
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar is open before window is closed.",
+ )
+
+ self.marionette.restart()
+ self.marionette.set_context("chrome")
+
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Windows from last session have been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar has been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return window.document.getElementById("sidebar-box").style.width;
+ """
+ ),
+ "100px",
+ "Sidebar width been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ const lazy = {};
+ ChromeUtils.defineESModuleGetters(lazy, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ });
+ let state = SessionStore.getCurrentState();
+ return state.windows[0].sidebar.command;
+ """
+ ),
+ "viewHistorySidebar",
+ "Correct sidebar category has been restored.",
+ )
diff --git a/browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py b/browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py
new file mode 100644
index 0000000000..58a9b93b47
--- /dev/null
+++ b/browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 0.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/0.0/.
+
+import os
+import sys
+
+# add this directory to the path
+sys.path.append(os.path.dirname(__file__))
+
+from session_store_test_case import SessionStoreTestCase
+
+
+def inline(title):
+ return "data:text/html;charset=utf-8,<html><head><title>{}</title></head><body></body></html>".format(
+ title
+ )
+
+
+class TestSessionRestore(SessionStoreTestCase):
+ """
+ Test that the sidebar and its attributes are restored on reopening of window.
+ """
+
+ def setUp(self):
+ super(TestSessionRestore, self).setUp(
+ startup_page=3,
+ include_private=False,
+ restore_on_demand=False,
+ test_windows=set(
+ [
+ (
+ inline("lorem ipsom"),
+ inline("dolor"),
+ ),
+ ]
+ ),
+ )
+
+ def test_restore(self):
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Should have 1 window open.",
+ )
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ window.SidebarController.show("viewHistorySidebar");
+ let sidebarBox = window.document.getElementById("sidebar-box")
+ sidebarBox.style.width = "100px";
+ """
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar is open before window is closed.",
+ )
+
+ self.marionette.restart()
+ self.marionette.set_context("chrome")
+
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Windows from last session have been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar has been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return window.document.getElementById("sidebar-box").style.width;
+ """
+ ),
+ "100px",
+ "Sidebar width been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ const lazy = {};
+ ChromeUtils.defineESModuleGetters(lazy, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ });
+ let state = SessionStore.getCurrentState();
+ return state.windows[0].sidebar.command;
+ """
+ ),
+ "viewHistorySidebar",
+ "Correct sidebar category has been restored.",
+ )