From a90a5cba08fdf6c0ceb95101c275108a152a3aed Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:35:37 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- .../RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs | 6 +- .../components/sessionstore/SessionFile.sys.mjs | 20 +- .../components/sessionstore/SessionLogger.sys.mjs | 87 +++++++ .../components/sessionstore/SessionStartup.sys.mjs | 68 ++--- .../components/sessionstore/SessionStore.sys.mjs | 286 ++++++++++----------- .../sessionstore/SessionStoreFunctions.sys.mjs | 93 +++++++ browser/components/sessionstore/components.conf | 12 + browser/components/sessionstore/moz.build | 6 + .../sessionstore/test/marionette/manifest.toml | 4 + .../test/marionette/test_restore_sidebar.py | 110 ++++++++ .../marionette/test_restore_sidebar_automatic.py | 110 ++++++++ 11 files changed, 617 insertions(+), 185 deletions(-) create mode 100644 browser/components/sessionstore/SessionLogger.sys.mjs create mode 100644 browser/components/sessionstore/SessionStoreFunctions.sys.mjs create mode 100644 browser/components/sessionstore/components.conf create mode 100644 browser/components/sessionstore/test/marionette/test_restore_sidebar.py create mode 100644 browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py (limited to 'browser/components/sessionstore') 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,12 +5603,33 @@ 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 @@ -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,{}".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,{}".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.", + ) -- cgit v1.2.3