From 9ead74bd1009b2d5aacb4f7e1c12c35b45bfa356 Mon Sep 17 00:00:00 2001 From: Alphan Chen Date: Tue, 3 Mar 2020 16:16:03 +0000 Subject: [PATCH] Bug 1507287 - Make sessionRestore work with session history living in the parent process. r=peterv Differential Revision: https://phabricator.services.mozilla.com/D46281 --HG-- extra : moz-landing-system : lando --- .../performance/browser_startup_content.js | 5 +- .../sessionstore/ContentRestore.jsm | 123 ++++-- .../sessionstore/ContentSessionStore.jsm | 369 ++++++---------- .../sessionstore/SessionHistoryListener.jsm | 232 ++++++++++ .../components/sessionstore/SessionStore.jsm | 397 +++++++++++++++++- browser/components/sessionstore/moz.build | 1 + .../test/browser_async_remove_tab.js | 23 +- .../test/browser_async_window_flushing.js | 8 +- browser/components/sessionstore/test/head.js | 4 + dom/base/nsFrameLoader.cpp | 12 + dom/base/nsFrameLoader.h | 2 + dom/ipc/BrowserChild.cpp | 13 +- dom/ipc/BrowserChild.h | 2 + dom/ipc/BrowserParent.cpp | 7 +- dom/ipc/BrowserParent.h | 3 +- dom/ipc/PBrowser.ipdl | 4 +- dom/webidl/FrameLoader.webidl | 5 + .../sessionstore/SessionStoreFunctions.idl | 4 +- .../sessionstore/SessionStoreFunctions.jsm | 16 +- .../sessionstore/SessionStoreListener.cpp | 44 +- .../sessionstore/SessionStoreListener.h | 29 +- .../modules/sessionstore/SessionHistory.jsm | 41 +- 22 files changed, 1034 insertions(+), 310 deletions(-) create mode 100644 browser/components/sessionstore/SessionHistoryListener.jsm diff --git a/browser/base/content/test/performance/browser_startup_content.js b/browser/base/content/test/performance/browser_startup_content.js index cf639af999a4..51f24879b90d 100644 --- a/browser/base/content/test/performance/browser_startup_content.js +++ b/browser/base/content/test/performance/browser_startup_content.js @@ -37,7 +37,6 @@ const whitelist = { // Session store "resource:///modules/sessionstore/ContentSessionStore.jsm", - "resource://gre/modules/sessionstore/SessionHistory.jsm", // Browser front-end "resource:///actors/AboutReaderChild.jsm", @@ -86,6 +85,10 @@ const intermittently_loaded_whitelist = { "resource://gre/modules/nsAsyncShutdown.jsm", "resource://gre/modules/sessionstore/Utils.jsm", + // Session store + "resource://gre/modules/sessionstore/SessionHistory.jsm", + "resource:///modules/sessionstore/SessionHistoryListener.jsm", + "resource://specialpowers/SpecialPowersChild.jsm", "resource://specialpowers/WrapPrivileged.jsm", diff --git a/browser/components/sessionstore/ContentRestore.jsm b/browser/components/sessionstore/ContentRestore.jsm index e179edb44c17..133cbab10ce7 100644 --- a/browser/components/sessionstore/ContentRestore.jsm +++ b/browser/components/sessionstore/ContentRestore.jsm @@ -58,6 +58,8 @@ function ContentRestore(chromeGlobal) { let EXPORTED_METHODS = [ "restoreHistory", + "finishRestoreHistory", + "restoreOnNewEntry", "restoreTabContent", "restoreDocument", "resetRestore", @@ -94,6 +96,26 @@ function ContentRestoreInternal(chromeGlobal) { // data from the network. Set in restoreHistory() and restoreTabContent(), // removed in resetRestore(). this._progressListener = null; + + this._shistoryInParent = false; + this._callbacksForBottomHalf = null; +} + +function kickOffNewLoadFromBlankPage(webNavigation, newURI) { + // Reset the tab's URL to what it's actually showing. Without this loadURI() + // would use the current document and change the displayed URL only. + webNavigation.setCurrentURI(Services.io.newURI("about:blank")); + + // Kick off a new load so that we navigate away from about:blank to the + // new URL that was passed to loadURI(). The new load will cause a + // STATE_START notification to be sent and the ProgressListener will then + // notify the parent and do the rest. + let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + let loadURIOptions = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + loadFlags, + }; + webNavigation.loadURI(newURI, loadURIOptions); } /** @@ -111,8 +133,9 @@ ContentRestoreInternal.prototype = { * non-zero) is passed through to all the callbacks. If a load in the tab * is started while it is pending, the appropriate callbacks are called. */ - restoreHistory(tabData, loadArguments, callbacks) { + restoreHistory(tabData, loadArguments, callbacks, shistoryInParent) { this._tabData = tabData; + this._callbacksForBottomHalf = callbacks; // In case about:blank isn't done yet. let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); @@ -130,27 +153,46 @@ ContentRestoreInternal.prototype = { webNavigation.setCurrentURI(Services.io.newURI(uri)); } - SessionHistory.restore(this.docShell, tabData); + this._shistoryInParent = shistoryInParent; - // Add a listener to watch for reloads. - let listener = new HistoryListener(this.docShell, () => { - // On reload, restore tab contents. - this.restoreTabContent(null, false, callbacks.onLoadFinished); - }); + if (this._shistoryInParent) { + callbacks.requestRestoreSHistory(); + } else { + SessionHistory.restore(this.docShell, tabData); - webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener); - this._historyListener = listener; + // Add a listener to watch for reloads. + let listener = new HistoryListener(this.docShell, () => { + // On reload, restore tab contents. + this.restoreTabContent( + null, + false, + callbacks.onLoadFinished, + null, + null + ); + }); + webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener); + this._historyListener = listener; + + this.finishRestoreHistory(); + } + }, + + finishRestoreHistory() { // Make sure to reset the capabilities and attributes in case this tab gets // reused. SessionStoreUtils.restoreDocShellCapabilities( this.docShell, - tabData.disallow + this._tabData.disallow ); - if (tabData.storage && this.docShell instanceof Ci.nsIDocShell) { - SessionStoreUtils.restoreSessionStorage(this.docShell, tabData.storage); - delete tabData.storage; + if (this._tabData.storage && this.docShell instanceof Ci.nsIDocShell) { + SessionStoreUtils.restoreSessionStorage( + this.docShell, + this._tabData.storage + ); + delete this._tabData.storage; } // Add a progress listener to correctly handle browser.loadURI() @@ -162,19 +204,33 @@ ContentRestoreInternal.prototype = { this._tabData = null; // Listen for the tab to finish loading. - this.restoreTabContentStarted(callbacks.onLoadFinished); + this.restoreTabContentStarted( + this._callbacksForBottomHalf.onLoadFinished, + this._callbacksForBottomHalf.removeRestoreListener + ); // Notify the parent. - callbacks.onLoadStarted(); + this._callbacksForBottomHalf.onLoadStarted(); }, }); }, + restoreOnNewEntry(newURI) { + let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); + kickOffNewLoadFromBlankPage(webNavigation, newURI); + }, + /** * Start loading the current page. When the data has finished loading from the * network, finishCallback is called. Returns true if the load was successful. */ - restoreTabContent(loadArguments, isRemotenessUpdate, finishCallback) { + restoreTabContent( + loadArguments, + isRemotenessUpdate, + finishCallback, + removeListenerCallback, + reloadSHistoryCallback + ) { let tabData = this._tabData; this._tabData = null; @@ -182,7 +238,7 @@ ContentRestoreInternal.prototype = { let history = webNavigation.sessionHistory.legacySHistory; // Listen for the tab to finish loading. - this.restoreTabContentStarted(finishCallback); + this.restoreTabContentStarted(finishCallback, removeListenerCallback); // Reset the current URI to about:blank. We changed it above for // switch-to-tab, but now it must go back to the correct value before the @@ -273,7 +329,11 @@ ContentRestoreInternal.prototype = { // In order to work around certain issues in session history, we need to // force session history to update its internal index and call reload // instead of gotoIndex. See bug 597315. - history.reloadCurrentEntry(); + if (this._shistoryInParent) { + reloadSHistoryCallback(); + } else { + history.reloadCurrentEntry(); + } } else { // If there's nothing to restore, we should still blank the page. let loadURIOptions = { @@ -298,10 +358,14 @@ ContentRestoreInternal.prototype = { * To be called after restoreHistory(). Removes all listeners needed for * pending tabs and makes sure to notify when the tab finished loading. */ - restoreTabContentStarted(finishCallback) { + restoreTabContentStarted(finishCallback, removeListenerCallback) { // The reload listener is no longer needed. - this._historyListener.uninstall(); - this._historyListener = null; + if (!this._shistoryInParent) { + this._historyListener.uninstall(); + this._historyListener = null; + } else { + removeListenerCallback(); + } // Remove the old progress listener. this._progressListener.uninstall(); @@ -371,6 +435,8 @@ ContentRestoreInternal.prototype = { this._progressListener.uninstall(); } this._progressListener = null; + + this._callbacksForBottomHalf = null; }, }; @@ -415,20 +481,7 @@ HistoryListener.prototype = { return; } - // Reset the tab's URL to what it's actually showing. Without this loadURI() - // would use the current document and change the displayed URL only. - this.webNavigation.setCurrentURI(Services.io.newURI("about:blank")); - - // Kick off a new load so that we navigate away from about:blank to the - // new URL that was passed to loadURI(). The new load will cause a - // STATE_START notification to be sent and the ProgressListener will then - // notify the parent and do the rest. - let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; - let loadURIOptions = { - triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), - loadFlags, - }; - this.webNavigation.loadURI(newURI.spec, loadURIOptions); + kickOffNewLoadFromBlankPage(this.webNavigation, newURI); }, OnHistoryReload() { diff --git a/browser/components/sessionstore/ContentSessionStore.jsm b/browser/components/sessionstore/ContentSessionStore.jsm index 14a223adb113..36438ab0e6c6 100644 --- a/browser/components/sessionstore/ContentSessionStore.jsm +++ b/browser/components/sessionstore/ContentSessionStore.jsm @@ -21,8 +21,8 @@ ChromeUtils.defineModuleGetter( ); ChromeUtils.defineModuleGetter( this, - "SessionHistory", - "resource://gre/modules/sessionstore/SessionHistory.jsm" + "SessionHistoryListener", + "resource:///modules/sessionstore/SessionHistoryListener.jsm" ); // This pref controls whether or not we send updates to the parent on a timeout @@ -31,9 +31,6 @@ const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates"; const PREF_INTERVAL = "browser.sessionstore.interval"; -const kNoIndex = Number.MAX_SAFE_INTEGER; -const kLastIndex = Number.MAX_SAFE_INTEGER - 1; - class Handler { constructor(store) { this.store = store; @@ -54,83 +51,8 @@ class Handler { get messageQueue() { return this.store.messageQueue; } - - get stateChangeNotifier() { - return this.store.stateChangeNotifier; - } } -/** - * Listens for state change notifcations from webProgress and notifies each - * registered observer for either the start of a page load, or its completion. - */ -class StateChangeNotifier extends Handler { - constructor(store) { - super(store); - - this._observers = new Set(); - let ifreq = this.mm.docShell.QueryInterface(Ci.nsIInterfaceRequestor); - let webProgress = ifreq.getInterface(Ci.nsIWebProgress); - webProgress.addProgressListener( - this, - Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT - ); - } - - /** - * Adds a given observer |obs| to the set of observers that will be notified - * when when a new document starts or finishes loading. - * - * @param obs (object) - */ - addObserver(obs) { - this._observers.add(obs); - } - - /** - * Notifies all observers that implement the given |method|. - * - * @param method (string) - */ - notifyObservers(method) { - for (let obs of this._observers) { - if (typeof obs[method] == "function") { - obs[method](); - } - } - } - - /** - * @see nsIWebProgressListener.onStateChange - */ - onStateChange(webProgress, request, stateFlags, status) { - // Ignore state changes for subframes because we're only interested in the - // top-document starting or stopping its load. - if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) { - return; - } - - // onStateChange will be fired when loading the initial about:blank URI for - // a browser, which we don't actually care about. This is particularly for - // the case of unrestored background tabs, where the content has not yet - // been restored: we don't want to accidentally send any updates to the - // parent when the about:blank placeholder page has loaded. - if (!this.mm.docShell.hasLoadedNonBlankURI) { - return; - } - - if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { - this.notifyObservers("onPageLoadStarted"); - } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { - this.notifyObservers("onPageLoadCompleted"); - } - } -} -StateChangeNotifier.prototype.QueryInterface = ChromeUtils.generateQI([ - Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference, -]); - /** * Listens for and handles content events that we need for the * session store service to be notified of state changes in content. @@ -177,139 +99,6 @@ class EventListener extends Handler { } } -/** - * Listens for changes to the session history. Whenever the user navigates - * we will collect URLs and everything belonging to session history. - * - * Causes a SessionStore:update message to be sent that contains the current - * session history. - * - * Example: - * {entries: [{url: "about:mozilla", ...}, ...], index: 1} - */ -class SessionHistoryListener extends Handler { - constructor(store) { - super(store); - - this._fromIdx = kNoIndex; - - // The state change observer is needed to handle initial subframe loads. - // It will redundantly invalidate with the SHistoryListener in some cases - // but these invalidations are very cheap. - this.stateChangeNotifier.addObserver(this); - - // By adding the SHistoryListener immediately, we will unfortunately be - // notified of every history entry as the tab is restored. We don't bother - // waiting to add the listener later because these notifications are cheap. - // We will likely only collect once since we are batching collection on - // a delay. - this.mm.docShell - .QueryInterface(Ci.nsIWebNavigation) - .sessionHistory.legacySHistory.addSHistoryListener(this); - - // Collect data if we start with a non-empty shistory. - if (!SessionHistory.isEmpty(this.mm.docShell)) { - this.collect(); - // When a tab is detached from the window, for the new window there is a - // new SessionHistoryListener created. Normally it is empty at this point - // but in a test env. the initial about:blank might have a children in which - // case we fire off a history message here with about:blank in it. If we - // don't do it ASAP then there is going to be a browser swap and the parent - // will be all confused by that message. - this.messageQueue.send(); - } - - // Listen for page title changes. - this.mm.addEventListener("DOMTitleChanged", this); - } - - uninit() { - let sessionHistory = this.mm.docShell.QueryInterface(Ci.nsIWebNavigation) - .sessionHistory; - if (sessionHistory) { - sessionHistory.legacySHistory.removeSHistoryListener(this); - } - } - - collect() { - // We want to send down a historychange even for full collects in case our - // session history is a partial session history, in which case we don't have - // enough information for a full update. collectFrom(-1) tells the collect - // function to collect all data avaliable in this process. - if (this.mm.docShell) { - this.collectFrom(-1); - } - } - - // History can grow relatively big with the nested elements, so if we don't have to, we - // don't want to send the entire history all the time. For a simple optimization - // we keep track of the smallest index from after any change has occured and we just send - // the elements from that index. If something more complicated happens we just clear it - // and send the entire history. We always send the additional info like the current selected - // index (so for going back and forth between history entries we set the index to kLastIndex - // if nothing else changed send an empty array and the additonal info like the selected index) - collectFrom(idx) { - if (this._fromIdx <= idx) { - // If we already know that we need to update history fromn index N we can ignore any changes - // tha happened with an element with index larger than N. - // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything - // here, and in case of navigation in the history back and forth we use kLastIndex which ignores - // only the subsequent navigations, but not any new elements added. - return; - } - - this._fromIdx = idx; - this.messageQueue.push("historychange", () => { - if (this._fromIdx === kNoIndex) { - return null; - } - - let history = SessionHistory.collect(this.mm.docShell, this._fromIdx); - this._fromIdx = kNoIndex; - return history; - }); - } - - handleEvent(event) { - this.collect(); - } - - onPageLoadCompleted() { - this.collect(); - } - - onPageLoadStarted() { - this.collect(); - } - - OnHistoryNewEntry(newURI, oldIndex) { - // We ought to collect the previously current entry as well, see bug 1350567. - this.collectFrom(oldIndex); - } - - OnHistoryGotoIndex() { - // We ought to collect the previously current entry as well, see bug 1350567. - this.collectFrom(kLastIndex); - } - - OnHistoryPurge() { - this.collect(); - } - - OnHistoryReload() { - this.collect(); - return true; - } - - OnHistoryReplaceEntry() { - this.collect(); - } -} -SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([ - Ci.nsISHistoryListener, - Ci.nsISupportsWeakReference, -]); - /** * A message queue that takes collected data and will take care of sending it * to the chrome process. It allows flushing using synchronous messages and @@ -550,6 +339,9 @@ class MessageQueue extends Handler { */ const MESSAGES = [ "SessionStore:restoreHistory", + "SessionStore:finishRestoreHistory", + "SessionStore:OnHistoryReload", + "SessionStore:OnHistoryNewEntry", "SessionStore:restoreTabContent", "SessionStore:resetRestore", "SessionStore:flush", @@ -560,23 +352,30 @@ class ContentSessionStore { constructor(mm) { this.mm = mm; this.messageQueue = new MessageQueue(this); - this.stateChangeNotifier = new StateChangeNotifier(this); this.epoch = 0; this.contentRestoreInitialized = false; + this.waitRestoreSHistoryInParent = false; + this.restoreTabContentData = null; + XPCOMUtils.defineLazyGetter(this, "contentRestore", () => { this.contentRestoreInitialized = true; return new ContentRestore(mm); }); - this.handlers = [ - new EventListener(this), - new SessionHistoryListener(this), - this.stateChangeNotifier, - this.messageQueue, - ]; + this.handlers = [new EventListener(this), this.messageQueue]; + + this._shistoryInParent = Services.prefs.getBoolPref( + "fission.sessionHistoryInParent", + false + ); + if (this._shistoryInParent) { + this.mm.sendAsyncMessage("SessionStore:addSHistoryListener"); + } else { + this.handlers.push(new SessionHistoryListener(this)); + } MESSAGES.forEach(m => mm.addMessageListener(m, this)); @@ -597,7 +396,7 @@ class ContentSessionStore { // override that to signal a new era in this tab's life. This enables it // to ignore async messages that were already sent but not yet received // and would otherwise confuse the internal tab state. - if (data.epoch && data.epoch != this.epoch) { + if (data && data.epoch && data.epoch != this.epoch) { this.epoch = data.epoch; } @@ -605,8 +404,53 @@ class ContentSessionStore { case "SessionStore:restoreHistory": this.restoreHistory(data); break; + case "SessionStore:finishRestoreHistory": + this.contentRestore.finishRestoreHistory(); + this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", { + epoch: this.epoch, + }); + if (this.restoreTabContentData) { + this.restoreTabContent(this.restoreTabContentData); + this.restoreTabContentData = null; + } + this.waitRestoreSHistoryInParent = false; + break; + case "SessionStore:OnHistoryNewEntry": + this.contentRestore.restoreOnNewEntry(data.uri); + break; + case "SessionStore:OnHistoryReload": + // On reload, restore tab contents. + this.contentRestore.restoreTabContent( + null, + false, + () => { + // Tell SessionStore.jsm that it may want to restore some more tabs, + // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. + this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { + epoch: this.epoch, + }); + }, + () => { + // Tell SessionStore.jsm to remove restoreListener + this.mm.sendAsyncMessage("SessionStore:removeRestoreListener", { + epoch: this.epoch, + }); + }, + () => { + // Tell SessionStore.jsm to reload currentEntry. + this.mm.sendAsyncMessage("SessionStore:reloadCurrentEntry", { + epoch: this.epoch, + }); + } + ); + break; case "SessionStore:restoreTabContent": - this.restoreTabContent(data); + if (this.waitRestoreSHistoryInParent) { + // queue the TabContentData if we haven't finished sHistoryRestore yet + this.restoreTabContentData = data; + } else { + this.restoreTabContent(data); + } break; case "SessionStore:resetRestore": this.contentRestore.resetRestore(); @@ -615,7 +459,9 @@ class ContentSessionStore { this.flush(data); break; case "SessionStore:becomeActiveProcess": - SessionHistoryListener.collect(); + if (!this._shistoryInParent) { + SessionHistoryListener.collect(); + } break; default: debug("received unknown message '" + name + "'"); @@ -624,27 +470,55 @@ class ContentSessionStore { } restoreHistory({ epoch, tabData, loadArguments, isRemotenessUpdate }) { - this.contentRestore.restoreHistory(tabData, loadArguments, { - // Note: The callbacks passed here will only be used when a load starts - // that was not initiated by sessionstore itself. This can happen when - // some code calls browser.loadURI() or browser.reload() on a pending - // browser/tab. + this.contentRestore.restoreHistory( + tabData, + loadArguments, + { + // Note: The callbacks passed here will only be used when a load starts + // that was not initiated by sessionstore itself. This can happen when + // some code calls browser.loadURI() or browser.reload() on a pending + // browser/tab. - onLoadStarted: () => { - // Notify the parent that the tab is no longer pending. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", { - epoch, - }); - }, + onLoadStarted: () => { + // Notify the parent that the tab is no longer pending. + this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", { + epoch, + }); + }, - onLoadFinished: () => { - // Tell SessionStore.jsm that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { - epoch, - }); + onLoadFinished: () => { + // Tell SessionStore.jsm that it may want to restore some more tabs, + // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. + this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { + epoch, + }); + }, + + removeRestoreListener: () => { + if (!this._shistoryInParent) { + return; + } + + // Notify the parent that the tab is no longer pending. + this.mm.sendAsyncMessage("SessionStore:removeRestoreListener", { + epoch, + }); + }, + + requestRestoreSHistory: () => { + if (!this._shistoryInParent) { + return; + } + + this.waitRestoreSHistoryInParent = true; + // Send tabData to the parent process. + this.mm.sendAsyncMessage("SessionStore:restoreSHistoryInParent", { + epoch, + }); + }, }, - }); + this._shistoryInParent + ); if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { // For non-remote tabs, when restoreHistory finishes, we send a synchronous @@ -661,7 +535,7 @@ class ContentSessionStore { epoch, isRemotenessUpdate, }); - } else { + } else if (!this._shistoryInParent) { this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", { epoch, isRemotenessUpdate, @@ -683,6 +557,17 @@ class ContentSessionStore { epoch, isRemotenessUpdate, }); + }, + () => { + // Tell SessionStore.jsm to remove restore listener. + this.mm.sendAsyncMessage("SessionStore:removeRestoreListener", { + epoch, + }); + }, + () => { + this.mm.sendAsyncMessage("SessionStore:reloadCurrentEntry", { + epoch, + }); } ); diff --git a/browser/components/sessionstore/SessionHistoryListener.jsm b/browser/components/sessionstore/SessionHistoryListener.jsm new file mode 100644 index 000000000000..067092e3047c --- /dev/null +++ b/browser/components/sessionstore/SessionHistoryListener.jsm @@ -0,0 +1,232 @@ +/* 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/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["SessionHistoryListener"]; + +ChromeUtils.defineModuleGetter( + this, + "SessionHistory", + "resource://gre/modules/sessionstore/SessionHistory.jsm" +); + +const kNoIndex = Number.MAX_SAFE_INTEGER; +const kLastIndex = Number.MAX_SAFE_INTEGER - 1; + +/** + * Listens for state change notifcations from webProgress and notifies each + * registered observer for either the start of a page load, or its completion. + */ +class StateChangeNotifier { + constructor(store) { + // super(store); + this.store = store; + + this._observers = new Set(); + // let ifreq = this.mm.docShell.QueryInterface(Ci.nsIInterfaceRequestor); + let ifreq = this.store.mm.docShell.QueryInterface(Ci.nsIInterfaceRequestor); + let webProgress = ifreq.getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener( + this, + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT + ); + } + + get mm() { + return this.store.mm; + } + + /** + * Adds a given observer |obs| to the set of observers that will be notified + * when when a new document starts or finishes loading. + * + * @param obs (object) + */ + addObserver(obs) { + this._observers.add(obs); + } + + /** + * Notifies all observers that implement the given |method|. + * + * @param method (string) + */ + notifyObservers(method) { + for (let obs of this._observers) { + if (typeof obs[method] == "function") { + obs[method](); + } + } + } + + /** + * @see nsIWebProgressListener.onStateChange + */ + onStateChange(webProgress, request, stateFlags, status) { + // Ignore state changes for subframes because we're only interested in the + // top-document starting or stopping its load. + if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) { + return; + } + + // onStateChange will be fired when loading the initial about:blank URI for + // a browser, which we don't actually care about. This is particularly for + // the case of unrestored background tabs, where the content has not yet + // been restored: we don't want to accidentally send any updates to the + // parent when the about:blank placeholder page has loaded. + if (!this.mm.docShell.hasLoadedNonBlankURI) { + return; + } + + if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { + this.notifyObservers("onPageLoadStarted"); + } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + this.notifyObservers("onPageLoadCompleted"); + } + } +} +StateChangeNotifier.prototype.QueryInterface = ChromeUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, +]); + +/** + * Listens for changes to the session history. Whenever the user navigates + * we will collect URLs and everything belonging to session history. + * + * Causes a SessionStore:update message to be sent that contains the current + * session history. + * + * Example: + * {entries: [{url: "about:mozilla", ...}, ...], index: 1} + */ +class SessionHistoryListener { + constructor(store) { + // super(store); + this.store = store; + + this._fromIdx = kNoIndex; + + // The state change observer is needed to handle initial subframe loads. + // It will redundantly invalidate with the SHistoryListener in some cases + // but these invalidations are very cheap. + this.stateChangeNotifier = new StateChangeNotifier(this); + this.stateChangeNotifier.addObserver(this); + + // By adding the SHistoryListener immediately, we will unfortunately be + // notified of every history entry as the tab is restored. We don't bother + // waiting to add the listener later because these notifications are cheap. + // We will likely only collect once since we are batching collection on + // a delay. + this.store.mm.docShell + .QueryInterface(Ci.nsIWebNavigation) + .sessionHistory.legacySHistory.addSHistoryListener(this); + + // Collect data if we start with a non-empty shistory. + if (!SessionHistory.isEmpty(this.store.mm.docShell)) { + this.collect(); + // When a tab is detached from the window, for the new window there is a + // new SessionHistoryListener created. Normally it is empty at this point + // but in a test env. the initial about:blank might have a children in which + // case we fire off a history message here with about:blank in it. If we + // don't do it ASAP then there is going to be a browser swap and the parent + // will be all confused by that message. + this.store.messageQueue.send(); + } + + // Listen for page title changes. + this.store.mm.addEventListener("DOMTitleChanged", this); + } + + get mm() { + return this.store.mm; + } + + uninit() { + let sessionHistory = this.mm.docShell.QueryInterface(Ci.nsIWebNavigation) + .sessionHistory; + if (sessionHistory) { + sessionHistory.legacySHistory.removeSHistoryListener(this); + } + } + + collect() { + // We want to send down a historychange even for full collects in case our + // session history is a partial session history, in which case we don't have + // enough information for a full update. collectFrom(-1) tells the collect + // function to collect all data avaliable in this process. + if (this.mm.docShell) { + this.collectFrom(-1); + } + } + + // History can grow relatively big with the nested elements, so if we don't have to, we + // don't want to send the entire history all the time. For a simple optimization + // we keep track of the smallest index from after any change has occured and we just send + // the elements from that index. If something more complicated happens we just clear it + // and send the entire history. We always send the additional info like the current selected + // index (so for going back and forth between history entries we set the index to kLastIndex + // if nothing else changed send an empty array and the additonal info like the selected index) + collectFrom(idx) { + if (this._fromIdx <= idx) { + // If we already know that we need to update history fromn index N we can ignore any changes + // tha happened with an element with index larger than N. + // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything + // here, and in case of navigation in the history back and forth we use kLastIndex which ignores + // only the subsequent navigations, but not any new elements added. + return; + } + + this._fromIdx = idx; + this.store.messageQueue.push("historychange", () => { + if (this._fromIdx === kNoIndex) { + return null; + } + + let history = SessionHistory.collect(this.mm.docShell, this._fromIdx); + this._fromIdx = kNoIndex; + return history; + }); + } + + handleEvent(event) { + this.collect(); + } + + onPageLoadCompleted() { + this.collect(); + } + + onPageLoadStarted() { + this.collect(); + } + + OnHistoryNewEntry(newURI, oldIndex) { + // We ought to collect the previously current entry as well, see bug 1350567. + this.collectFrom(oldIndex); + } + + OnHistoryGotoIndex() { + // We ought to collect the previously current entry as well, see bug 1350567. + this.collectFrom(kLastIndex); + } + + OnHistoryPurge() { + this.collect(); + } + + OnHistoryReload() { + this.collect(); + return true; + } + + OnHistoryReplaceEntry() { + this.collect(); + } +} +SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([ + Ci.nsISHistoryListener, + Ci.nsISupportsWeakReference, +]); diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm index 3812cec5532f..3739b29df366 100644 --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -97,6 +97,22 @@ const MESSAGES = [ // The content script encountered an error. "SessionStore:error", + + // The content script asks us to add the session history listener in the + // parent process when sessionHistory is in the parent process. + "SessionStore:addSHistoryListener", + + // The content script asks us to remove the session history listener which + // is added in the restore process when sessionHistory is in the parent process. + "SessionStore:removeRestoreListener", + + // The content script asks us to restore session history in the parent process + // when sessionHistory is in the parent process. + "SessionStore:restoreSHistoryInParent", + + // The content script asks us to reload the current session history entry when + // sessionHistory is in the parent process. + "SessionStore:reloadCurrentEntry", ]; // The list of messages we accept from s that have no tab @@ -112,6 +128,9 @@ const NOTAB_MESSAGES = new Set([ // For a description see above. "SessionStore:error", + + // For a description see above. + "SessionStore:addSHistoryListener", ]); // The list of messages we accept without an "epoch" parameter. @@ -122,6 +141,9 @@ const NOEPOCH_MESSAGES = new Set([ // For a description see above. "SessionStore:error", + + // For a description see above. + "SessionStore:addSHistoryListener", ]); // The list of messages we want to receive even during the short period after a @@ -175,6 +197,10 @@ const RESTORE_TAB_CONTENT_REASON = { // 'browser.startup.page' preference value to resume the previous session. const BROWSER_STARTUP_RESUME_SESSION = 3; +// for session history listener +const kNoIndex = Number.MAX_SAFE_INTEGER; +const kLastIndex = Number.MAX_SAFE_INTEGER - 1; + ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this); ChromeUtils.import("resource://gre/modules/Services.jsm", this); ChromeUtils.import("resource://gre/modules/TelemetryTimestamps.jsm", this); @@ -182,6 +208,12 @@ ChromeUtils.import("resource://gre/modules/Timer.jsm", this); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); ChromeUtils.import("resource://gre/modules/osfile.jsm", this); +ChromeUtils.defineModuleGetter( + this, + "SessionHistory", + "resource://gre/modules/sessionstore/SessionHistory.jsm" +); + XPCOMUtils.defineLazyServiceGetters(this, { gScreenManager: ["@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"], Telemetry: ["@mozilla.org/base/telemetry;1", "nsITelemetry"], @@ -501,6 +533,15 @@ var SessionStoreInternal = { // windows yet to be restored _restoreCount: -1, + // For each element, records the SHistoryListener. + _browserSHistoryListener: new WeakMap(), + + // For each element, records the SHistoryListener. + _browserSHistoryListenerForRestore: new WeakMap(), + + // The history data needed to be restored in the parent + _shistoryToRestore: new WeakMap(), + // For each element, records the current epoch. _browserEpochs: new WeakMap(), @@ -824,6 +865,11 @@ var SessionStoreInternal = { "privacy.resistFingerprinting" ); Services.prefs.addObserver("privacy.resistFingerprinting", this); + + this._shistoryInParent = Services.prefs.getBoolPref( + "fission.sessionHistoryInParent", + false + ); }, /** @@ -907,6 +953,211 @@ var SessionStoreInternal = { } }, + // Create a sHistoryLister and register it. + // Also need to save the SHistoryLister into this._browserSHistoryListener + addSHistoryListener(aBrowser) { + function SHistoryListener(browser) { + browser.frameLoader.browsingContext.sessionHistory.addSHistoryListener( + this + ); + + this.browser = browser; + this.frameLoader = browser.frameLoader; + this._fromIdx = kNoIndex; + this._sHistoryChanges = false; + if (this.browser.currentURI && this.browser.ownerGlobal) { + this._lastKnownUri = browser.currentURI.displaySpec; + this._lastKnownBody = browser.ownerGlobal.document.body; + this._lastKnownUserContextId = + browser.contentPrincipal.originAttributes.userContextId; + } + } + SHistoryListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + Ci.nsISHistoryListener, + Ci.nsISupportsWeakReference, + ]), + + notifySHistoryChanges(index) { + if (this._fromIdx <= index) { + // If we already know that we need to update history from index N we can ignore any changes + // that happened with an element with index larger than N. + // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything + // here, and in case of navigation in the history back and forth we use kLastIndex which ignores + // only the subsequent navigations, but not any new elements added. + return; + } + + if (!this._sHistoryChanges) { + this.frameLoader.requestSHistoryUpdate(/*aImmediately*/ false); + this._sHistoryChanges = true; + } + this._fromIdx = index; + if (this.browser.currentURI && this.browser.ownerGlobal) { + this._lastKnownUri = this.browser.currentURI.displaySpec; + this._lastKnownBody = this.browser.ownerGlobal.document.body; + this._lastKnownUserContextId = this.browser.contentPrincipal.originAttributes.userContextId; + } + }, + + uninstall() { + if (this.frameLoader.browsingContext) { + let shistory = this.frameLoader.browsingContext.sessionHistory; + if (shistory) { + shistory.removeSHistoryListener(this); + } + } + }, + + OnHistoryNewEntry(newURI, oldIndex) { + this.notifySHistoryChanges(oldIndex); + }, + + OnHistoryGotoIndex() { + this.notifySHistoryChanges(kLastIndex); + }, + OnHistoryPurge() { + this.notifySHistoryChanges(-1); + }, + + OnHistoryReload() { + this.notifySHistoryChanges(-1); + return true; + }, + + OnHistoryReplaceEntry() { + this.notifySHistoryChanges(-1); + + let win = this.browser.ownerGlobal; + let tab = win ? win.gBrowser.getTabForBrowser(this.browser) : null; + if (tab) { + let event = tab.ownerDocument.createEvent("CustomEvent"); + event.initCustomEvent("SSHistoryReplaceEntry", true, false); + tab.dispatchEvent(event); + } + }, + }; + + let spec = null; + if (aBrowser.currentURI) { + spec = aBrowser.currentURI.displaySpec; + } + + if (!aBrowser.frameLoader) { + dump( + "====DEBUG==== addSHistoryListener(), aBrowser.frameLoader not exists" + + ",browser.currentURI.displaySpec=" + + spec + + "\n" + ); + return; + } + if (!aBrowser.frameLoader.browsingContext) { + dump( + "====DEBUG==== addSHistoryListener(), aBrowser.fl.browsingContext not exists" + + ",browser.currentURI.displaySpec=" + + spec + + "\n" + ); + return; + } + if (!aBrowser.frameLoader.browsingContext.sessionHistory) { + dump( + "====DEBUG==== addSHistoryListener(), aBrowser.fl.bc.sessionHistory not exists" + + ",browser.currentURI.displaySpec=" + + spec + + "\n" + ); + return; + } + + let listener = new SHistoryListener(aBrowser); + this._browserSHistoryListener.set(aBrowser.permanentKey, listener); + + // Collect data if we start with a non-empty shistory. + let uri = aBrowser.currentURI.displaySpec; + let history = aBrowser.frameLoader.browsingContext.sessionHistory; + if (uri != "about:blank" || history.count != 0) { + aBrowser.frameLoader.requestSHistoryUpdate(/*aImmediately*/ true); + } + }, + + /* + * This listener detects when a page being restored is reloaded. It triggers a + * callback and cancels the reload. The callback will send a message to + * SessionStore.jsm so that it can restore the content immediately. + */ + addSHistoryListenerForRestore(aBrowser) { + function SHistoryListener(browser) { + browser.frameLoader.browsingContext.sessionHistory.addSHistoryListener( + this + ); + this.browser = browser; + } + SHistoryListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + Ci.nsISHistoryListener, + Ci.nsISupportsWeakReference, + ]), + + uninstall() { + let shistory = this.browser.frameLoader.browsingContext.sessionHistory; + if (shistory) { + shistory.removeSHistoryListener(this); + } + }, + + OnHistoryGotoIndex() {}, + OnHistoryPurge() {}, + OnHistoryReplaceEntry() {}, + + // This will be called for a pending tab when loadURI(uri) is called where + // the given |uri| only differs in the fragment. + OnHistoryNewEntry(newURI) { + // Need to do something + let currentURI = this.browser.currentURI; + + // Ignore new SHistory entries with the same URI as those do not indicate + // a navigation inside a document by changing the #hash part of the URL. + // We usually hit this when purging session history for browsers. + if (currentURI && currentURI.displaySpec == newURI.spec) { + return; + } + + // notify ContentSessionStore.jsm to restore tab contents. + this.browser.messageManager.sendAsyncMessage( + "SessionStore:OnHistoryNewEntry", + { uri: newURI.spec } + ); + }, + + OnHistoryReload() { + // notify ContentSessionStore.jsm to restore tab contents. + this.browser.messageManager.sendAsyncMessage( + "SessionStore:OnHistoryReload" + ); + // Cancel the load. + return false; + }, + }; + + if (!aBrowser.frameLoader) { + return; + } + if (!aBrowser.frameLoader.browsingContext) { + return; + } + if (!aBrowser.frameLoader.browsingContext.sessionHistory) { + return; + } + + let listener = new SHistoryListener(aBrowser); + this._browserSHistoryListenerForRestore.set( + aBrowser.permanentKey, + listener + ); + }, + updateSessionStoreFromTablistener(aBrowser, aData) { if (aBrowser.permanentKey == undefined) { return; @@ -917,6 +1168,72 @@ var SessionStoreInternal = { return; } + let sHistoryChangedInListener = false; + let listener = this._browserSHistoryListener.get(aBrowser.permanentKey); + if (listener) { + sHistoryChangedInListener = listener._sHistoryChanges; + } + + if (aData.sHistoryNeeded || sHistoryChangedInListener) { + if (!listener) { + dump( + "====DEBUG==== @ SessionStore.jsm updateSessionStoreFromTablistener() with aData.sHistoryNeeded, but no SHlistener. Add again!!!\n" + ); + this.addSHistoryListener(aBrowser); + listener = this._browserSHistoryListener.get(aBrowser.permanentKey); + } + + if (listener) { + if (!aData.sHistoryNeeded && listener._fromIdx == kNoIndex) { + // no shistory changes needed + listener._sHistoryChanges = false; + } else { + // |browser.frameLoader| might be empty if the browser was already + // destroyed and its tab removed. In that case we still have the last + // frameLoader we know about to compare. + let frameLoader = + aBrowser.frameLoader || + this._lastKnownFrameLoader.get(aBrowser.permanentKey); + if ( + frameLoader && + frameLoader.browsingContext && + frameLoader.browsingContext.sessionHistory + ) { + let uri = aBrowser.currentURI + ? aBrowser.currentURI.displaySpec + : listener._lastKnownUri; + let body = aBrowser.ownerGlobal + ? aBrowser.ownerGlobal.document.body + : listener._lastKnownBody; + let userContextId = aBrowser.contentPrincipal + ? aBrowser.contentPrincipal.originAttributes.userContextId + : listener._lastKnownUserContextId; + aData.data.historychange = SessionHistory.collectFromParent( + uri, + body, + frameLoader.browsingContext.sessionHistory, + userContextId, + listener._sHistoryChanges ? listener._fromIdx : -1 + ); + listener._sHistoryChanges = false; + listener._fromIdx = kNoIndex; + } else { + dump( + "====DEBUG==== @SessionStore.jsm:updateSessionStoreFromTablistener() with sHistoryNeeded, but no fL.bC.sessionHistory\n" + ); + } + } + } else { + dump( + "====DEBUG==== @ SessionStore.jsm updateSessionStoreFromTablistener() with sHistoryNeeded, but no sHlistener!!!\n" + ); + } + } + + if ("sHistoryNeeded" in aData) { + delete aData.sHistoryNeeded; + } + TabState.update(aBrowser, aData); let win = aBrowser.ownerGlobal; this.saveStateDelayed(win); @@ -965,6 +1282,34 @@ var SessionStoreInternal = { } switch (aMessage.name) { + case "SessionStore:addSHistoryListener": + this.addSHistoryListener(browser); + break; + case "SessionStore:restoreSHistoryInParent": + if ( + browser.frameLoader && + browser.frameLoader.browsingContext && + browser.frameLoader.browsingContext.sessionHistory + ) { + let tabData = this._shistoryToRestore.get(browser.permanentKey); + if (tabData) { + this._shistoryToRestore.delete(browser.permanentKey); + SessionHistory.restoreFromParent( + browser.frameLoader.browsingContext.sessionHistory, + tabData + ); + } + this.addSHistoryListenerForRestore(browser); + } else { + dump( + "====DEBUG==== @SessionStore.jsm receive SessionStore:restoreSHistoryInParent: but cannot find sessionHistory from bc\n" + ); + } + browser.messageManager.sendAsyncMessage( + "SessionStore:finishRestoreHistory" + ); + break; + case "SessionStore:update": // |browser.frameLoader| might be empty if the browser was already // destroyed and its tab removed. In that case we still have the last @@ -984,6 +1329,13 @@ var SessionStoreInternal = { // late and will never respond. If they have been sent shortly after // switching a browser's remoteness there isn't too much data to skip. TabStateFlusher.resolveAll(browser); + let listener = this._browserSHistoryListener.get( + browser.permanentKey + ); + if (listener) { + listener.uninstall(); + this._browserSHistoryListener.delete(browser.permanentKey); + } } else if (aMessage.data.flushID) { // This is an update kicked off by an async flush request. Notify the // TabStateFlusher so that it can finish the request and notify its @@ -1073,6 +1425,39 @@ var SessionStoreInternal = { tab.dispatchEvent(event); break; } + case "SessionStore:removeRestoreListener": + let listener = this._browserSHistoryListenerForRestore.get( + browser.permanentKey + ); + if (listener) { + listener.uninstall(); + this._browserSHistoryListenerForRestore.delete(browser.permanentKey); + } + break; + case "SessionStore:reloadCurrentEntry": + let fL = + browser.frameLoader || + this._lastKnownFrameLoader.get(browser.permanentKey); + if (fL) { + if (fL.browsingContext) { + if (fL.browsingContext.sessionHistory) { + fL.browsingContext.sessionHistory.reloadCurrentEntry(); + } else { + dump( + "====DEBUG==== receive SessionStore:reloadCurrentEntry browser.fL.bC.sessionHistory is null\n" + ); + } + } else { + dump( + "====DEBUG==== receive SessionStore:reloadCurrentEntry browser.fL.browsingContext is null\n" + ); + } + } else { + dump( + "====DEBUG==== receive SessionStore:reloadCurrentEntry browser.frameLoader is null\n" + ); + } + break; case "SessionStore:restoreTabContentStarted": if (TAB_STATE_FOR_BROWSER.get(browser) == TAB_STATE_NEEDS_RESTORE) { // If a load not initiated by sessionstore was started in a @@ -1115,9 +1500,7 @@ var SessionStoreInternal = { SessionStoreInternal._resetLocalTabRestoringState(tab); SessionStoreInternal.restoreNextTab(); - this._sendTabRestoredNotification(tab, data.isRemotenessUpdate); - Services.obs.notifyObservers( null, "sessionstore-one-or-no-tab-restored" @@ -1211,6 +1594,11 @@ var SessionStoreInternal = { epoch: newEpoch, } ); + + let listener = this._browserSHistoryListener.get(target.permanentKey); + if (listener) { + listener.notifySHistoryChanges(-1); + } break; default: throw new Error(`unhandled event ${aEvent.type}?`); @@ -5796,6 +6184,11 @@ var SessionStoreInternal = { } } + if (this._shistoryInParent) { + // save the history data for restoring in the parent process + this._shistoryToRestore.set(browser.permanentKey, options.tabData); + } + browser.messageManager.sendAsyncMessage( "SessionStore:restoreHistory", options diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build index 07006d8d06b2..55c26cd3db49 100644 --- a/browser/components/sessionstore/moz.build +++ b/browser/components/sessionstore/moz.build @@ -17,6 +17,7 @@ EXTRA_JS_MODULES.sessionstore = [ 'RunState.jsm', 'SessionCookies.jsm', 'SessionFile.jsm', + 'SessionHistoryListener.jsm', 'SessionMigration.jsm', 'SessionSaver.jsm', 'SessionStartup.jsm', diff --git a/browser/components/sessionstore/test/browser_async_remove_tab.js b/browser/components/sessionstore/test/browser_async_remove_tab.js index 625c3a7cd91f..b3a1f60c83b2 100644 --- a/browser/components/sessionstore/test/browser_async_remove_tab.js +++ b/browser/components/sessionstore/test/browser_async_remove_tab.js @@ -32,12 +32,26 @@ function restoreClosedTabWithValue(rval) { return ss.undoCloseTab(window, index); } -function promiseNewLocationAndHistoryEntryReplaced(browser, snippet) { +function promiseNewLocationAndHistoryEntryReplaced(tab, snippet) { + let browser = tab.linkedBrowser; + + if (Services.prefs.getBoolPref("fission.sessionHistoryInParent", false)) { + SpecialPowers.spawn(browser, [snippet], async function(codeSnippet) { + // Need to define 'webNavigation' for 'codeSnippet' + // eslint-disable-next-line no-unused-vars + let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + // Evaluate the snippet that changes the location. + // eslint-disable-next-line no-eval + eval(codeSnippet); + }); + return promiseOnHistoryReplaceEntry(tab); + } + return SpecialPowers.spawn(browser, [snippet], async function(codeSnippet) { let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); let shistory = webNavigation.sessionHistory.legacySHistory; - // Evaluate the snippet that the changes the location. + // Evaluate the snippet that changes the location. // eslint-disable-next-line no-eval eval(codeSnippet); @@ -121,7 +135,7 @@ add_task(async function save_worthy_tabs_remote_final() { let snippet = 'webNavigation.loadURI("https://example.com/",\ {triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()})'; - await promiseNewLocationAndHistoryEntryReplaced(browser, snippet); + await promiseNewLocationAndHistoryEntryReplaced(tab, snippet); // Remotness shouldn't have changed. ok(browser.isRemoteBrowser, "browser is still remote"); @@ -164,11 +178,10 @@ add_task(async function save_worthy_tabs_nonremote_final() { add_task(async function dont_save_empty_tabs_final() { let { tab, r } = await createTabWithRandomValue("https://example.com/"); - let browser = tab.linkedBrowser; // Replace the current page with an about:blank entry. let snippet = 'content.location.replace("about:blank")'; - await promiseNewLocationAndHistoryEntryReplaced(browser, snippet); + await promiseNewLocationAndHistoryEntryReplaced(tab, snippet); // Remove the tab before the update arrives. let promise = promiseRemoveTabAndSessionState(tab); diff --git a/browser/components/sessionstore/test/browser_async_window_flushing.js b/browser/components/sessionstore/test/browser_async_window_flushing.js index ddbea009aabf..f7106344d246 100644 --- a/browser/components/sessionstore/test/browser_async_window_flushing.js +++ b/browser/components/sessionstore/test/browser_async_window_flushing.js @@ -38,8 +38,12 @@ add_task(async function test_add_interesting_window() { content.location = newPage; }); - await promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry"); - + if (Services.prefs.getBoolPref("fission.sessionHistoryInParent", false)) { + let tab = newWin.gBrowser.selectedTab; + await promiseOnHistoryReplaceEntry(tab); + } else { + await promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry"); + } // Clear out the userTypedValue so that the new window looks like // it's really not worth restoring. browser.userTypedValue = null; diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js index b0841a4fa8e2..fad7a8e5e8b7 100644 --- a/browser/components/sessionstore/test/head.js +++ b/browser/components/sessionstore/test/head.js @@ -556,6 +556,10 @@ function promiseDelayedStartupFinished(aWindow) { return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve)); } +function promiseOnHistoryReplaceEntry(tab) { + return BrowserTestUtils.waitForEvent(tab, "SSHistoryReplaceEntry"); +} + function promiseTabRestored(tab) { return BrowserTestUtils.waitForEvent(tab, "SSTabRestored"); } diff --git a/dom/base/nsFrameLoader.cpp b/dom/base/nsFrameLoader.cpp index 142281177754..118f58cdc734 100644 --- a/dom/base/nsFrameLoader.cpp +++ b/dom/base/nsFrameLoader.cpp @@ -3199,6 +3199,18 @@ void nsFrameLoader::RequestEpochUpdate(uint32_t aEpoch) { } } +void nsFrameLoader::RequestSHistoryUpdate(bool aImmediately) { + if (mSessionStoreListener) { + mSessionStoreListener->UpdateSHistoryChanges(aImmediately); + return; + } + + // If remote browsing (e10s), handle this with the BrowserParent. + if (auto* browserParent = GetBrowserParent()) { + Unused << browserParent->SendUpdateSHistory(aImmediately); + } +} + void nsFrameLoader::Print(uint64_t aOuterWindowID, nsIPrintSettings* aPrintSettings, nsIWebProgressListener* aProgressListener, diff --git a/dom/base/nsFrameLoader.h b/dom/base/nsFrameLoader.h index 47b80cecceed..7201b3fed288 100644 --- a/dom/base/nsFrameLoader.h +++ b/dom/base/nsFrameLoader.h @@ -211,6 +211,8 @@ class nsFrameLoader final : public nsStubMutationObserver, void RequestEpochUpdate(uint32_t aEpoch); + void RequestSHistoryUpdate(bool aImmediately = false); + void Print(uint64_t aOuterWindowID, nsIPrintSettings* aPrintSettings, nsIWebProgressListener* aProgressListener, mozilla::ErrorResult& aRv); diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp index 930864447de7..fb0a8067ccc5 100644 --- a/dom/ipc/BrowserChild.cpp +++ b/dom/ipc/BrowserChild.cpp @@ -2013,6 +2013,14 @@ mozilla::ipc::IPCResult BrowserChild::RecvUpdateEpoch(const uint32_t& aEpoch) { return IPC_OK(); } +mozilla::ipc::IPCResult BrowserChild::RecvUpdateSHistory( + const bool& aImmediately) { + if (mSessionStoreListener) { + mSessionStoreListener->UpdateSHistoryChanges(aImmediately); + } + return IPC_OK(); +} + // In case handling repeated keys takes much time, we skip firing new ones. bool BrowserChild::SkipRepeatedKeyEvent(const WidgetKeyboardEvent& aEvent) { if (mRepeatedKeyEventTime.IsNull() || !aEvent.CanSkipInRemoteProcess() || @@ -3938,8 +3946,9 @@ bool BrowserChild::UpdateSessionStore(uint32_t aFlushId, bool aIsFinal) { Unused << SendSessionStoreUpdate( docShellCaps, privatedMode, positions, positionDescendants, inputs, - idVals, xPathVals, origins, keys, values, isFullStorage, aFlushId, - aIsFinal, mSessionStoreListener->GetEpoch()); + idVals, xPathVals, origins, keys, values, isFullStorage, + store->GetAndClearSHistoryChanged(), aFlushId, aIsFinal, + mSessionStoreListener->GetEpoch()); return true; } diff --git a/dom/ipc/BrowserChild.h b/dom/ipc/BrowserChild.h index 55954441e57b..394872f10282 100644 --- a/dom/ipc/BrowserChild.h +++ b/dom/ipc/BrowserChild.h @@ -368,6 +368,8 @@ class BrowserChild final : public nsMessageManagerScriptExecutor, mozilla::ipc::IPCResult RecvUpdateEpoch(const uint32_t& aEpoch); + mozilla::ipc::IPCResult RecvUpdateSHistory(const bool& aImmediately); + mozilla::ipc::IPCResult RecvNativeSynthesisResponse( const uint64_t& aObserverId, const nsCString& aResponse); diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp index 9af9644093a4..60f95d00aa62 100644 --- a/dom/ipc/BrowserParent.cpp +++ b/dom/ipc/BrowserParent.cpp @@ -2805,7 +2805,8 @@ mozilla::ipc::IPCResult BrowserParent::RecvSessionStoreUpdate( const nsTArray& aXPathVals, nsTArray&& aOrigins, nsTArray&& aKeys, nsTArray&& aValues, const bool aIsFullStorage, - const uint32_t& aFlushId, const bool& aIsFinal, const uint32_t& aEpoch) { + const bool aNeedCollectSHistory, const uint32_t& aFlushId, + const bool& aIsFinal, const uint32_t& aEpoch) { UpdateSessionStoreData data; if (aDocShellCaps.isSome()) { data.mDocShellCaps.Construct() = aDocShellCaps.value(); @@ -2862,8 +2863,8 @@ mozilla::ipc::IPCResult BrowserParent::RecvSessionStoreUpdate( bool ok = ToJSValue(jsapi.cx(), data, &dataVal); NS_ENSURE_TRUE(ok, IPC_OK()); - nsresult rv = funcs->UpdateSessionStore(mFrameElement, aFlushId, aIsFinal, - aEpoch, dataVal); + nsresult rv = funcs->UpdateSessionStore( + mFrameElement, aFlushId, aIsFinal, aEpoch, dataVal, aNeedCollectSHistory); NS_ENSURE_SUCCESS(rv, IPC_OK()); return IPC_OK(); diff --git a/dom/ipc/BrowserParent.h b/dom/ipc/BrowserParent.h index 079c249e723a..d15ec7bb3dd8 100644 --- a/dom/ipc/BrowserParent.h +++ b/dom/ipc/BrowserParent.h @@ -340,7 +340,8 @@ class BrowserParent final : public PBrowserParent, const nsTArray& aXPathVals, nsTArray&& aOrigins, nsTArray&& aKeys, nsTArray&& aValues, const bool aIsFullStorage, - const uint32_t& aFlushId, const bool& aIsFinal, const uint32_t& aEpoch); + const bool aNeedCollectSHistory, const uint32_t& aFlushId, + const bool& aIsFinal, const uint32_t& aEpoch); mozilla::ipc::IPCResult RecvBrowserFrameOpenWindow( PBrowserParent* aOpener, const nsString& aURL, const nsString& aName, diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl index 07d10db0320c..17288a0e5233 100644 --- a/dom/ipc/PBrowser.ipdl +++ b/dom/ipc/PBrowser.ipdl @@ -614,12 +614,14 @@ parent: CollectedInputDataValue[] aXPathVals, nsCString[] aOrigins, nsString[] aKeys, nsString[] aValues, bool aIsFullStorage, - uint32_t aFlushId, bool aIsFinal, uint32_t aEpoch); + bool aNeedCollectSHistory, uint32_t aFlushId, + bool aIsFinal, uint32_t aEpoch); child: async NativeSynthesisResponse(uint64_t aObserverId, nsCString aResponse); async FlushTabState(uint32_t aFlushId, bool aIsFinal); async UpdateEpoch(uint32_t aEpoch); + async UpdateSHistory(bool aImmediately); parent: diff --git a/dom/webidl/FrameLoader.webidl b/dom/webidl/FrameLoader.webidl index e55547bb62b1..851fc6537894 100644 --- a/dom/webidl/FrameLoader.webidl +++ b/dom/webidl/FrameLoader.webidl @@ -112,6 +112,11 @@ interface FrameLoader { */ void requestEpochUpdate(unsigned long aEpoch); + /** + * Request a session history update in native sessionStoreListeners. + */ + void requestSHistoryUpdate(boolean aImmediately); + /** * Print the current document. * diff --git a/toolkit/components/sessionstore/SessionStoreFunctions.idl b/toolkit/components/sessionstore/SessionStoreFunctions.idl index 5ae04d7e8559..35a47cc4737f 100644 --- a/toolkit/components/sessionstore/SessionStoreFunctions.idl +++ b/toolkit/components/sessionstore/SessionStoreFunctions.idl @@ -11,5 +11,7 @@ webidl Element; interface nsISessionStoreFunctions : nsISupports { // update sessionStore from the tabListener implemented by C++ // aData is a UpdateSessionStoreData dictionary (From SessionStoreUtils.webidl) - void UpdateSessionStore(in Element aBrowser, in uint32_t aFlushId, in boolean aIsFinal, in uint32_t aEpoch, in jsval aData); + void UpdateSessionStore( + in Element aBrowser, in uint32_t aFlushId, in boolean aIsFinal, + in uint32_t aEpoch, in jsval aData, in boolean aCollectSHistory); }; diff --git a/toolkit/components/sessionstore/SessionStoreFunctions.jsm b/toolkit/components/sessionstore/SessionStoreFunctions.jsm index c887af745600..378af2210fb1 100644 --- a/toolkit/components/sessionstore/SessionStoreFunctions.jsm +++ b/toolkit/components/sessionstore/SessionStoreFunctions.jsm @@ -8,13 +8,21 @@ XPCOMUtils.defineLazyModuleGetters(this, { SessionStore: "resource:///modules/sessionstore/SessionStore.jsm", }); -function UpdateSessionStore(aBrowser, aFlushId, aIsFinal, aEpoch, aData) { +function UpdateSessionStore( + aBrowser, + aFlushId, + aIsFinal, + aEpoch, + aData, + aCollectSHistory +) { return SessionStoreFuncInternal.updateSessionStore( aBrowser, aFlushId, aIsFinal, aEpoch, - aData + aData, + aCollectSHistory ); } @@ -382,7 +390,8 @@ var SessionStoreFuncInternal = { aFlushId, aIsFinal, aEpoch, - aData + aData, + aCollectSHistory ) { let currentData = {}; if (aData.docShellCaps != undefined) { @@ -434,6 +443,7 @@ var SessionStoreFuncInternal = { flushID: aFlushId, isFinal: aIsFinal, epoch: aEpoch, + sHistoryNeeded: aCollectSHistory, }); this._formDataId = []; this._formDataIdValue = []; diff --git a/toolkit/components/sessionstore/SessionStoreListener.cpp b/toolkit/components/sessionstore/SessionStoreListener.cpp index f33260010582..e8367041fac8 100644 --- a/toolkit/components/sessionstore/SessionStoreListener.cpp +++ b/toolkit/components/sessionstore/SessionStoreListener.cpp @@ -9,6 +9,7 @@ #include "mozilla/dom/SessionStoreUtilsBinding.h" #include "mozilla/dom/StorageEvent.h" #include "mozilla/dom/BrowserChild.h" +#include "mozilla/StaticPrefs_fission.h" #include "nsGenericHTMLElement.h" #include "nsDocShell.h" #include "nsIAppWindow.h" @@ -44,7 +45,10 @@ ContentSessionStore::ContentSessionStore(nsIDocShell* aDocShell) mScrollChanged(NO_CHANGE), mFormDataChanged(NO_CHANGE), mStorageStatus(NO_STORAGE), - mDocCapChanged(false) { + mDocCapChanged(false), + mSHistoryInParent(StaticPrefs::fission_sessionHistoryInParent()), + mSHistoryChanged(false), + mSHistoryChangedFromParent(false) { MOZ_ASSERT(mDocShell); // Check that value at startup as it might have // been set before the frame script was loaded. @@ -123,11 +127,19 @@ void ContentSessionStore::OnDocumentStart() { } SetFullStorageNeeded(); + + if (mSHistoryInParent) { + mSHistoryChanged = true; + } } void ContentSessionStore::OnDocumentEnd() { mScrollChanged = WITH_CHANGE; SetFullStorageNeeded(); + + if (mSHistoryInParent) { + mSHistoryChanged = true; + } } NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TabListener) @@ -156,7 +168,8 @@ TabListener::TabListener(nsIDocShell* aDocShell, Element* aElement) mUpdatedTimer(nullptr), mTimeoutDisabled(false), mUpdateInterval(15000), - mEpoch(0) { + mEpoch(0), + mSHistoryInParent(StaticPrefs::fission_sessionHistoryInParent()) { MOZ_ASSERT(mDocShell); } @@ -206,6 +219,12 @@ nsresult TabListener::Init() { eventTarget->AddSystemEventListener(NS_LITERAL_STRING("mozvisualscroll"), this, false); eventTarget->AddSystemEventListener(NS_LITERAL_STRING("input"), this, false); + + if (mSHistoryInParent) { + eventTarget->AddSystemEventListener(NS_LITERAL_STRING("DOMTitleChanged"), + this, false); + } + mEventListenerRegistered = true; eventTarget->AddSystemEventListener( NS_LITERAL_STRING("MozSessionStorageChanged"), this, false); @@ -347,6 +366,9 @@ TabListener::HandleEvent(Event* aEvent) { if (mSessionStore->AppendSessionStorageChange(event)) { AddTimerForUpdate(); } + } else if (eventType.EqualsLiteral("DOMTitleChanged")) { + mSessionStore->SetSHistoryChanged(); + AddTimerForUpdate(); } return NS_OK; @@ -641,6 +663,15 @@ bool TabListener::ForceFlushFromParent(uint32_t aFlushId, bool aIsFinal) { return UpdateSessionStore(aFlushId, aIsFinal); } +void TabListener::UpdateSHistoryChanges(bool aImmediately) { + mSessionStore->SetSHistoryFromParentChanged(); + if (aImmediately) { + UpdateSessionStore(); + } else { + AddTimerForUpdate(); + } +} + bool TabListener::UpdateSessionStore(uint32_t aFlushId, bool aIsFinal) { if (!aFlushId) { if (!mSessionStore || !mSessionStore->UpdateNeeded()) { @@ -740,8 +771,9 @@ bool TabListener::UpdateSessionStore(uint32_t aFlushId, bool aIsFinal) { bool ok = ToJSValue(jsapi.cx(), data, &dataVal); NS_ENSURE_TRUE(ok, false); - nsresult rv = funcs->UpdateSessionStore(mOwnerContent, aFlushId, aIsFinal, - mEpoch, dataVal); + nsresult rv = funcs->UpdateSessionStore( + mOwnerContent, aFlushId, aIsFinal, mEpoch, dataVal, + mSessionStore->GetAndClearSHistoryChanged()); NS_ENSURE_SUCCESS(rv, false); StopTimerForUpdate(); return true; @@ -791,6 +823,10 @@ void TabListener::RemoveListeners() { NS_LITERAL_STRING("mozvisualscroll"), this, false); eventTarget->RemoveSystemEventListener(NS_LITERAL_STRING("input"), this, false); + if (mSHistoryInParent) { + eventTarget->RemoveSystemEventListener( + NS_LITERAL_STRING("DOMTitleChanged"), this, false); + } mEventListenerRegistered = false; } if (mStorageChangeListenerRegistered) { diff --git a/toolkit/components/sessionstore/SessionStoreListener.h b/toolkit/components/sessionstore/SessionStoreListener.h index aedbb44a3658..9f7d50e90627 100644 --- a/toolkit/components/sessionstore/SessionStoreListener.h +++ b/toolkit/components/sessionstore/SessionStoreListener.h @@ -65,11 +65,24 @@ class ContentSessionStore { // Return true if there is a new storage change which is appended. bool AppendSessionStorageChange(StorageEvent* aEvent); + void SetSHistoryChanged() { mSHistoryChanged = mSHistoryInParent; } + // request "collect sessionHistory" which is happened in the parent process + void SetSHistoryFromParentChanged() { + mSHistoryChangedFromParent = mSHistoryInParent; + } + bool GetAndClearSHistoryChanged() { + bool ret = mSHistoryChanged; + mSHistoryChanged = false; + mSHistoryChangedFromParent = false; + return ret; + } + void OnDocumentStart(); void OnDocumentEnd(); bool UpdateNeeded() { return mPrivateChanged || mDocCapChanged || IsScrollPositionChanged() || - IsFormDataChanged() || IsStorageUpdated(); + IsFormDataChanged() || IsStorageUpdated() || mSHistoryChanged || + mSHistoryChangedFromParent; } private: @@ -97,6 +110,17 @@ class ContentSessionStore { nsTArray mOrigins; nsTArray mKeys; nsTArray mValues; + // need to collect sessionHistory + bool mSHistoryInParent; + // mSHistoryChanged means there are history changes which are found + // in the child process. The flag is set when + // 1. webProgress changes to STATE_START + // 2. webProgress changes to STATE_STOP + // 3. receiving "DOMTitleChanged" event + bool mSHistoryChanged; + // mSHistoryChangedFromParent means there are history changes which + // are found by session history listener in the parent process. + bool mSHistoryChangedFromParent; }; class TabListener : public nsIDOMEventListener, @@ -114,6 +138,7 @@ class TabListener : public nsIDOMEventListener, void RemoveListeners(); void SetEpoch(uint32_t aEpoch) { mEpoch = aEpoch; } uint32_t GetEpoch() { return mEpoch; } + void UpdateSHistoryChanges(bool aImmediately); NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TabListener, nsIDOMEventListener) @@ -145,6 +170,8 @@ class TabListener : public nsIDOMEventListener, bool mTimeoutDisabled; int32_t mUpdateInterval; uint32_t mEpoch; + // sessionHistory in the parent process + bool mSHistoryInParent; }; } // namespace dom diff --git a/toolkit/modules/sessionstore/SessionHistory.jsm b/toolkit/modules/sessionstore/SessionHistory.jsm index c8da05d4dc3a..ed924e0039fc 100644 --- a/toolkit/modules/sessionstore/SessionHistory.jsm +++ b/toolkit/modules/sessionstore/SessionHistory.jsm @@ -39,9 +39,23 @@ var SessionHistory = Object.freeze({ return SessionHistoryInternal.collect(docShell, aFromIdx); }, + collectFromParent(uri, body, history, userContextId, aFromIdx = -1) { + return SessionHistoryInternal.collectCommon( + uri, + body, + history, + userContextId, + aFromIdx + ); + }, + restore(docShell, tabData) { return SessionHistoryInternal.restore(docShell, tabData); }, + + restoreFromParent(history, tabData) { + return SessionHistoryInternal.restoreCommon(history, tabData); + }, }); /** @@ -81,12 +95,24 @@ var SessionHistoryInternal = { collect(docShell, aFromIdx = -1) { let loadContext = docShell.QueryInterface(Ci.nsILoadContext); let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); + let uri = webNavigation.currentURI.displaySpec; + let body = webNavigation.document.body; let history = webNavigation.sessionHistory; + let userContextId = loadContext.originAttributes.userContextId; + return this.collectCommon( + uri, + body, + history.legacySHistory, + userContextId, + aFromIdx + ); + }, + collectCommon(uri, body, shistory, userContextId, aFromIdx) { let data = { entries: [], - userContextId: loadContext.originAttributes.userContextId, - requestedIndex: history.legacySHistory.requestedIndex + 1, + userContextId, + requestedIndex: shistory.requestedIndex + 1, }; // We want to keep track how many entries we *could* have collected and @@ -95,8 +121,7 @@ var SessionHistoryInternal = { let skippedCount = 0, entryCount = 0; - if (history && history.count > 0) { - let shistory = history.legacySHistory.QueryInterface(Ci.nsISHistory); + if (shistory && shistory.count > 0) { let count = shistory.count; for (; entryCount < count; entryCount++) { let shEntry = shistory.getEntryAtIndex(entryCount); @@ -109,15 +134,13 @@ var SessionHistoryInternal = { } // Ensure the index isn't out of bounds if an exception was thrown above. - data.index = Math.min(history.index + 1, entryCount); + data.index = Math.min(shistory.index + 1, entryCount); } // If either the session history isn't available yet or doesn't have any // valid entries, make sure we at least include the current page, // unless of course we just skipped all entries because aFromIdx was big enough. if (!data.entries.length && (skippedCount != entryCount || aFromIdx < 0)) { - let uri = webNavigation.currentURI.displaySpec; - let body = webNavigation.document.body; // We landed here because the history is inaccessible or there are no // history entries. In that case we should at least record the docShell's // current URL as a single history entry. If the URL is not about:blank @@ -337,6 +360,10 @@ var SessionHistoryInternal = { restore(docShell, tabData) { let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); let history = webNavigation.sessionHistory.legacySHistory; + this.restoreCommon(history, tabData); + }, + + restoreCommon(history, tabData) { if (history.count > 0) { history.PurgeHistory(history.count); }