Backed out 3 changesets (bug 1887029, bug 1886892) for causing failures at browser_all_files_referenced.js. CLOSED TREE

Backed out changeset 07f554ea4869 (bug 1887029)
Backed out changeset daa5f2355675 (bug 1886892)
Backed out changeset 389163cbd649 (bug 1886892)
This commit is contained in:
Butkovits Atila 2024-04-01 21:50:42 +03:00
parent 90eb86dbb0
commit 159929cd10
26 changed files with 2101 additions and 180 deletions

View file

@ -2051,6 +2051,18 @@
// doesn't keep the window alive.
b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
// Ensure that SessionStore has flushed any session history state from the
// content process before we this browser's remoteness.
if (!Services.appinfo.sessionHistoryInParent) {
b.prepareToChangeRemoteness = () =>
SessionStore.prepareToChangeRemoteness(b);
b.afterChangeRemoteness = switchId => {
let tab = this.getTabForBrowser(b);
SessionStore.finishTabRemotenessChange(tab, switchId);
return true;
};
}
const defaultBrowserAttributes = {
contextmenu: "contentAreaContextMenu",
message: "true",

View file

@ -57,6 +57,12 @@ const known_scripts = {
]),
};
if (!Services.appinfo.sessionHistoryInParent) {
known_scripts.modules.add(
"resource:///modules/sessionstore/ContentSessionStore.sys.mjs"
);
}
// Items on this list *might* load when creating the process, as opposed to
// items in the main list, which we expect will always load.
const intermittently_loaded_scripts = {

View file

@ -28,6 +28,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
@ -220,6 +221,7 @@ CustomizeMode.prototype = {
gTab = aTab;
gTab.setAttribute("customizemode", "true");
lazy.SessionStore.persistTabAttribute("customizemode");
if (gTab.linkedPanel) {
gTab.linkedBrowser.stop();

View file

@ -0,0 +1,435 @@
/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
Utils: "resource://gre/modules/sessionstore/Utils.sys.mjs",
});
/**
* This module implements the content side of session restoration. The chrome
* side is handled by SessionStore.sys.mjs. The functions in this module are called
* by content-sessionStore.js based on messages received from SessionStore.sys.mjs
* (or, in one case, based on a "load" event). Each tab has its own
* ContentRestore instance, constructed by content-sessionStore.js.
*
* In a typical restore, content-sessionStore.js will call the following based
* on messages and events it receives:
*
* restoreHistory(tabData, loadArguments, callbacks)
* Restores the tab's history and session cookies.
* restoreTabContent(loadArguments, finishCallback)
* Starts loading the data for the current page to restore.
* restoreDocument()
* Restore form and scroll data.
*
* When the page has been loaded from the network, we call finishCallback. It
* should send a message to SessionStore.sys.mjs, which may cause other tabs to be
* restored.
*
* When the page has finished loading, a "load" event will trigger in
* content-sessionStore.js, which will call restoreDocument. At that point,
* form data is restored and the restore is complete.
*
* At any time, SessionStore.sys.mjs can cancel the ongoing restore by sending a
* reset message, which causes resetRestore to be called. At that point it's
* legal to begin another restore.
*/
export function ContentRestore(chromeGlobal) {
let internal = new ContentRestoreInternal(chromeGlobal);
let external = {};
let EXPORTED_METHODS = [
"restoreHistory",
"restoreTabContent",
"restoreDocument",
"resetRestore",
];
for (let method of EXPORTED_METHODS) {
external[method] = internal[method].bind(internal);
}
return Object.freeze(external);
}
function ContentRestoreInternal(chromeGlobal) {
this.chromeGlobal = chromeGlobal;
// The following fields are only valid during certain phases of the restore
// process.
// The tabData for the restore. Set in restoreHistory and removed in
// restoreTabContent.
this._tabData = null;
// Contains {entry, scrollPositions, formdata}, where entry is a
// single entry from the tabData.entries array. Set in
// restoreTabContent and removed in restoreDocument.
this._restoringDocument = null;
// This listener is used to detect reloads on restoring tabs. Set in
// restoreHistory and removed in restoreTabContent.
this._historyListener = null;
// This listener detects when a pending tab starts loading (when not
// initiated by sessionstore) and when a restoring tab has finished loading
// data from the network. Set in restoreHistory() and restoreTabContent(),
// removed in resetRestore().
this._progressListener = null;
}
/**
* The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are
* public.
*/
ContentRestoreInternal.prototype = {
get docShell() {
return this.chromeGlobal.docShell;
},
/**
* Starts the process of restoring a tab. The tabData to be restored is passed
* in here and used throughout the restoration. The epoch (which must be
* 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) {
this._tabData = tabData;
// In case about:blank isn't done yet.
let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
// Make sure currentURI is set so that switch-to-tab works before the tab is
// restored. We'll reset this to about:blank when we try to restore the tab
// to ensure that docshell doeesn't get confused. Don't bother doing this if
// we're restoring immediately due to a process switch. It just causes the
// URL bar to be temporarily blank.
let activeIndex = tabData.index - 1;
let activePageData = tabData.entries[activeIndex] || {};
let uri = activePageData.url || null;
if (uri && !loadArguments) {
webNavigation.setCurrentURIForSessionStore(Services.io.newURI(uri));
}
lazy.SessionHistory.restore(this.docShell, tabData);
// Add a listener to watch for reloads.
let listener = new HistoryListener(this.docShell, () => {
// On reload, restore tab contents.
this.restoreTabContent(null, false, callbacks.onLoadFinished);
});
webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener);
this._historyListener = listener;
// Make sure to reset the capabilities and attributes in case this tab gets
// reused.
SessionStoreUtils.restoreDocShellCapabilities(
this.docShell,
tabData.disallow
);
// Add a progress listener to correctly handle browser.loadURI()
// calls from foreign code.
this._progressListener = new ProgressListener(this.docShell, {
onStartRequest: () => {
// Some code called browser.loadURI() on a pending tab. It's safe to
// assume we don't care about restoring scroll or form data.
this._tabData = null;
// Listen for the tab to finish loading.
this.restoreTabContentStarted(callbacks.onLoadFinished);
// Notify the parent.
callbacks.onLoadStarted();
},
});
},
/**
* 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) {
let tabData = this._tabData;
this._tabData = null;
let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
// Listen for the tab to finish loading.
this.restoreTabContentStarted(finishCallback);
// 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
// load happens. Don't bother doing this if we're restoring immediately
// due to a process switch.
if (!isRemotenessUpdate) {
webNavigation.setCurrentURIForSessionStore(
Services.io.newURI("about:blank")
);
}
try {
if (loadArguments) {
// If the load was started in another process, and the in-flight channel
// was redirected into this process, resume that load within our process.
//
// NOTE: In this case `isRemotenessUpdate` must be true.
webNavigation.resumeRedirectedLoad(
loadArguments.redirectLoadSwitchId,
loadArguments.redirectHistoryIndex
);
} else if (tabData.userTypedValue && tabData.userTypedClear) {
// If the user typed a URL into the URL bar and hit enter right before
// we crashed, we want to start loading that page again. A non-zero
// userTypedClear value means that the load had started.
// Load userTypedValue and fix up the URL if it's partial/broken.
let loadURIOptions = {
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
};
webNavigation.fixupAndLoadURIString(
tabData.userTypedValue,
loadURIOptions
);
} else if (tabData.entries.length) {
// Stash away the data we need for restoreDocument.
this._restoringDocument = {
formdata: tabData.formdata || {},
scrollPositions: tabData.scroll || {},
};
// 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.
let history = webNavigation.sessionHistory.legacySHistory;
history.reloadCurrentEntry();
} else {
// If there's nothing to restore, we should still blank the page.
let loadURIOptions = {
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
// Specify an override to force the load to finish in the current
// process, as tests rely on this behaviour for non-fission session
// restore.
remoteTypeOverride: Services.appinfo.remoteType,
};
webNavigation.loadURI(
Services.io.newURI("about:blank"),
loadURIOptions
);
}
return true;
} catch (ex) {
if (ex instanceof Ci.nsIException) {
// Ignore page load errors, but return false to signal that the load never
// happened.
return false;
}
}
return null;
},
/**
* To be called after restoreHistory(). Removes all listeners needed for
* pending tabs and makes sure to notify when the tab finished loading.
*/
restoreTabContentStarted(finishCallback) {
// The reload listener is no longer needed.
this._historyListener.uninstall();
this._historyListener = null;
// Remove the old progress listener.
this._progressListener.uninstall();
// We're about to start a load. This listener will be called when the load
// has finished getting everything from the network.
this._progressListener = new ProgressListener(this.docShell, {
onStopRequest: () => {
// Call resetRestore() to reset the state back to normal. The data
// needed for restoreDocument() (which hasn't happened yet) will
// remain in _restoringDocument.
this.resetRestore();
finishCallback();
},
});
},
/**
* Finish restoring the tab by filling in form data and setting the scroll
* position. The restore is complete when this function exits. It should be
* called when the "load" event fires for the restoring tab. Returns true
* if we're restoring a document.
*/
restoreDocument() {
if (!this._restoringDocument) {
return;
}
let { formdata, scrollPositions } = this._restoringDocument;
this._restoringDocument = null;
let window = this.docShell.domWindow;
// Restore form data.
lazy.Utils.restoreFrameTreeData(window, formdata, (frame, data) => {
// restore() will return false, and thus abort restoration for the
// current |frame| and its descendants, if |data.url| is given but
// doesn't match the loaded document's URL.
return SessionStoreUtils.restoreFormData(frame.document, data);
});
// Restore scroll data.
lazy.Utils.restoreFrameTreeData(window, scrollPositions, (frame, data) => {
if (data.scroll) {
SessionStoreUtils.restoreScrollPosition(frame, data);
}
});
},
/**
* Cancel an ongoing restore. This function can be called any time between
* restoreHistory and restoreDocument.
*
* This function is called externally (if a restore is canceled) and
* internally (when the loads for a restore have finished). In the latter
* case, it's called before restoreDocument, so it cannot clear
* _restoringDocument.
*/
resetRestore() {
this._tabData = null;
if (this._historyListener) {
this._historyListener.uninstall();
}
this._historyListener = null;
if (this._progressListener) {
this._progressListener.uninstall();
}
this._progressListener = null;
},
};
/*
* 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.sys.mjs so that it can restore the content immediately.
*/
function HistoryListener(docShell, callback) {
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
webNavigation.sessionHistory.legacySHistory.addSHistoryListener(this);
this.webNavigation = webNavigation;
this.callback = callback;
}
HistoryListener.prototype = {
QueryInterface: ChromeUtils.generateQI([
"nsISHistoryListener",
"nsISupportsWeakReference",
]),
uninstall() {
let shistory = this.webNavigation.sessionHistory.legacySHistory;
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) {
let currentURI = this.webNavigation.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.spec == newURI.spec) {
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.setCurrentURIForSessionStore(
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, loadURIOptions);
},
OnHistoryReload() {
this.callback();
// Cancel the load.
return false;
},
};
/**
* This class informs SessionStore.sys.mjs whenever the network requests for a
* restoring page have completely finished. We only restore three tabs
* simultaneously, so this is the signal for SessionStore.sys.mjs to kick off
* another restore (if there are more to do).
*
* The progress listener is also used to be notified when a load not initiated
* by sessionstore starts. Pending tabs will then need to be marked as no
* longer pending.
*/
function ProgressListener(docShell, callbacks) {
let webProgress = docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
this.webProgress = webProgress;
this.callbacks = callbacks;
}
ProgressListener.prototype = {
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
uninstall() {
this.webProgress.removeProgressListener(this);
},
onStateChange(webProgress, request, stateFlags, _status) {
let { STATE_IS_WINDOW, STATE_STOP, STATE_START } =
Ci.nsIWebProgressListener;
if (!webProgress.isTopLevel || !(stateFlags & STATE_IS_WINDOW)) {
return;
}
if (stateFlags & STATE_START && this.callbacks.onStartRequest) {
this.callbacks.onStartRequest();
}
if (stateFlags & STATE_STOP && this.callbacks.onStopRequest) {
this.callbacks.onStopRequest();
}
},
};

View file

@ -0,0 +1,685 @@
/* 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 {
clearTimeout,
setTimeoutWithTarget,
} from "resource://gre/modules/Timer.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ContentRestore: "resource:///modules/sessionstore/ContentRestore.sys.mjs",
SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
});
// This pref controls whether or not we send updates to the parent on a timeout
// or not, and should only be used for tests or debugging.
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;
}
get contentRestore() {
return this.store.contentRestore;
}
get contentRestoreInitialized() {
return this.store.contentRestoreInitialized;
}
get mm() {
return this.store.mm;
}
get messageQueue() {
return this.store.messageQueue;
}
}
/**
* Listens for and handles content events that we need for the
* session store service to be notified of state changes in content.
*/
class EventListener extends Handler {
constructor(store) {
super(store);
SessionStoreUtils.addDynamicFrameFilteredListener(
this.mm,
"load",
this,
true
);
}
handleEvent(event) {
let { content } = this.mm;
// Ignore load events from subframes.
if (event.target != content.document) {
return;
}
if (content.document.documentURI.startsWith("about:reader")) {
if (
event.type == "load" &&
!content.document.body.classList.contains("loaded")
) {
// Don't restore the scroll position of an about:reader page at this
// point; listen for the custom event dispatched from AboutReader.sys.mjs.
content.addEventListener("AboutReaderContentReady", this);
return;
}
content.removeEventListener("AboutReaderContentReady", this);
}
if (this.contentRestoreInitialized) {
// Restore the form data and scroll position.
this.contentRestore.restoreDocument();
}
}
}
/**
* 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;
// 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); // OK in non-geckoview
let webProgress = this.mm.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(
this,
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
);
// Collect data if we start with a non-empty shistory.
if (!lazy.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.store.messageQueue.send();
}
// Listen for page title changes.
this.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); // OK in non-geckoview
}
}
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 = lazy.SessionHistory.collect(
this.mm.docShell,
this._fromIdx
);
this._fromIdx = kNoIndex;
return history;
});
}
handleEvent() {
this.collect();
}
OnHistoryNewEntry(newURI, oldIndex) {
// Collect the current entry as well, to make sure to collect any changes
// that were made to the entry while the document was active.
this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1);
}
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();
}
/**
* @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.collect();
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
this.collect();
}
}
}
SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISHistoryListener",
"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
* takes care of any race conditions that might occur because of that. Changes
* will be batched if they're pushed in quick succession to avoid a message
* flood.
*/
class MessageQueue extends Handler {
constructor(store) {
super(store);
/**
* A map (string -> lazy fn) holding lazy closures of all queued data
* collection routines. These functions will return data collected from the
* docShell.
*/
this._data = new Map();
/**
* The delay (in ms) used to delay sending changes after data has been
* invalidated.
*/
this.BATCH_DELAY_MS = 1000;
/**
* The minimum idle period (in ms) we need for sending data to chrome process.
*/
this.NEEDED_IDLE_PERIOD_MS = 5;
/**
* Timeout for waiting an idle period to send data. We will set this from
* the pref "browser.sessionstore.interval".
*/
this._timeoutWaitIdlePeriodMs = null;
/**
* The current timeout ID, null if there is no queue data. We use timeouts
* to damp a flood of data changes and send lots of changes as one batch.
*/
this._timeout = null;
/**
* Whether or not sending batched messages on a timer is disabled. This should
* only be used for debugging or testing. If you need to access this value,
* you should probably use the timeoutDisabled getter.
*/
this._timeoutDisabled = false;
/**
* True if there is already a send pending idle dispatch, set to prevent
* scheduling more than one. If false there may or may not be one scheduled.
*/
this._idleScheduled = false;
this.timeoutDisabled = Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
this._timeoutWaitIdlePeriodMs = Services.prefs.getIntPref(PREF_INTERVAL);
Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this);
Services.prefs.addObserver(PREF_INTERVAL, this);
}
/**
* True if batched messages are not being fired on a timer. This should only
* ever be true when debugging or during tests.
*/
get timeoutDisabled() {
return this._timeoutDisabled;
}
/**
* Disables sending batched messages on a timer. Also cancels any pending
* timers.
*/
set timeoutDisabled(val) {
this._timeoutDisabled = val;
if (val && this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
}
uninit() {
Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this);
Services.prefs.removeObserver(PREF_INTERVAL, this);
this.cleanupTimers();
}
/**
* Cleanup pending idle callback and timer.
*/
cleanupTimers() {
this._idleScheduled = false;
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
}
observe(subject, topic, data) {
if (topic == "nsPref:changed") {
switch (data) {
case TIMEOUT_DISABLED_PREF:
this.timeoutDisabled = Services.prefs.getBoolPref(
TIMEOUT_DISABLED_PREF
);
break;
case PREF_INTERVAL:
this._timeoutWaitIdlePeriodMs =
Services.prefs.getIntPref(PREF_INTERVAL);
break;
default:
console.error("received unknown message '" + data + "'");
break;
}
}
}
/**
* Pushes a given |value| onto the queue. The given |key| represents the type
* of data that is stored and can override data that has been queued before
* but has not been sent to the parent process, yet.
*
* @param key (string)
* A unique identifier specific to the type of data this is passed.
* @param fn (function)
* A function that returns the value that will be sent to the parent
* process.
*/
push(key, fn) {
this._data.set(key, fn);
if (!this._timeout && !this._timeoutDisabled) {
// Wait a little before sending the message to batch multiple changes.
this._timeout = setTimeoutWithTarget(
() => this.sendWhenIdle(),
this.BATCH_DELAY_MS,
this.mm.tabEventTarget
);
}
}
/**
* Sends queued data when the remaining idle time is enough or waiting too
* long; otherwise, request an idle time again. If the |deadline| is not
* given, this function is going to schedule the first request.
*
* @param deadline (object)
* An IdleDeadline object passed by idleDispatch().
*/
sendWhenIdle(deadline) {
if (!this.mm.content) {
// The frameloader is being torn down. Nothing more to do.
return;
}
if (deadline) {
if (
deadline.didTimeout ||
deadline.timeRemaining() > this.NEEDED_IDLE_PERIOD_MS
) {
this.send();
return;
}
} else if (this._idleScheduled) {
// Bail out if there's a pending run.
return;
}
ChromeUtils.idleDispatch(deadline_ => this.sendWhenIdle(deadline_), {
timeout: this._timeoutWaitIdlePeriodMs,
});
this._idleScheduled = true;
}
/**
* Sends queued data to the chrome process.
*
* @param options (object)
* {flushID: 123} to specify that this is a flush
* {isFinal: true} to signal this is the final message sent on unload
*/
send(options = {}) {
// Looks like we have been called off a timeout after the tab has been
// closed. The docShell is gone now and we can just return here as there
// is nothing to do.
if (!this.mm.docShell) {
return;
}
this.cleanupTimers();
let flushID = (options && options.flushID) || 0;
let histID = "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_MS";
let data = {};
for (let [key, func] of this._data) {
if (key != "isPrivate") {
TelemetryStopwatch.startKeyed(histID, key);
}
let value = func();
if (key != "isPrivate") {
TelemetryStopwatch.finishKeyed(histID, key);
}
if (value || (key != "storagechange" && key != "historychange")) {
data[key] = value;
}
}
this._data.clear();
try {
// Send all data to the parent process.
this.mm.sendAsyncMessage("SessionStore:update", {
data,
flushID,
isFinal: options.isFinal || false,
epoch: this.store.epoch,
});
} catch (ex) {
if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) {
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM")
.add(1);
this.mm.sendAsyncMessage("SessionStore:error");
}
}
}
}
/**
* Listens for and handles messages sent by the session store service.
*/
const MESSAGES = [
"SessionStore:restoreHistory",
"SessionStore:restoreTabContent",
"SessionStore:resetRestore",
"SessionStore:flush",
"SessionStore:prepareForProcessChange",
];
export class ContentSessionStore {
constructor(mm) {
if (Services.appinfo.sessionHistoryInParent) {
throw new Error("This frame script should not be loaded for SHIP");
}
this.mm = mm;
this.messageQueue = new MessageQueue(this);
this.epoch = 0;
this.contentRestoreInitialized = false;
this.handlers = [
this.messageQueue,
new EventListener(this),
new SessionHistoryListener(this),
];
ChromeUtils.defineLazyGetter(this, "contentRestore", () => {
this.contentRestoreInitialized = true;
return new lazy.ContentRestore(mm);
});
MESSAGES.forEach(m => mm.addMessageListener(m, this));
mm.addEventListener("unload", this);
}
receiveMessage({ name, data }) {
// The docShell might be gone. Don't process messages,
// that will just lead to errors anyway.
if (!this.mm.docShell) {
return;
}
// A fresh tab always starts with epoch=0. The parent has the ability to
// 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 && data.epoch && data.epoch != this.epoch) {
this.epoch = data.epoch;
}
switch (name) {
case "SessionStore:restoreHistory":
this.restoreHistory(data);
break;
case "SessionStore:restoreTabContent":
this.restoreTabContent(data);
break;
case "SessionStore:resetRestore":
this.contentRestore.resetRestore();
break;
case "SessionStore:flush":
this.flush(data);
break;
case "SessionStore:prepareForProcessChange":
// During normal in-process navigations, the DocShell would take
// care of automatically persisting layout history state to record
// scroll positions on the nsSHEntry. Unfortunately, process switching
// is not a normal navigation, so for now we do this ourselves. This
// is a workaround until session history state finally lives in the
// parent process.
this.mm.docShell.persistLayoutHistoryState();
break;
default:
console.error("received unknown message '" + name + "'");
break;
}
}
// non-SHIP only
restoreHistory(data) {
let { epoch, tabData, loadArguments, isRemotenessUpdate } = data;
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,
});
},
onLoadFinished: () => {
// Tell SessionStore.sys.mjs 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,
});
},
});
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
// For non-remote tabs, when restoreHistory finishes, we send a synchronous
// message to SessionStore.sys.mjs so that it can run SSTabRestoring. Users of
// SSTabRestoring seem to get confused if chrome and content are out of
// sync about the state of the restore (particularly regarding
// docShell.currentURI). Using a synchronous message is the easiest way
// to temporarily synchronize them.
//
// For remote tabs, because all nsIWebProgress notifications are sent
// asynchronously using messages, we get the same-order guarantees of the
// message manager, and can use an async message.
this.mm.sendSyncMessage("SessionStore:restoreHistoryComplete", {
epoch,
isRemotenessUpdate,
});
} else {
this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", {
epoch,
isRemotenessUpdate,
});
}
}
restoreTabContent({ loadArguments, isRemotenessUpdate, reason }) {
let epoch = this.epoch;
// We need to pass the value of didStartLoad back to SessionStore.sys.mjs.
let didStartLoad = this.contentRestore.restoreTabContent(
loadArguments,
isRemotenessUpdate,
() => {
// Tell SessionStore.sys.mjs 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,
isRemotenessUpdate,
});
}
);
this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", {
epoch,
isRemotenessUpdate,
reason,
});
if (!didStartLoad) {
// Pretend that the load succeeded so that event handlers fire correctly.
this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {
epoch,
isRemotenessUpdate,
});
}
}
flush({ id }) {
// Flush the message queue, send the latest updates.
this.messageQueue.send({ flushID: id });
}
handleEvent(event) {
if (event.type == "unload") {
this.onUnload();
}
}
onUnload() {
// Upon frameLoader destruction, send a final update message to
// the parent and flush all data currently held in the child.
this.messageQueue.send({ isFinal: true });
for (let handler of this.handlers) {
if (handler.uninit) {
handler.uninit();
}
}
if (this.contentRestoreInitialized) {
// Remove progress listeners.
this.contentRestore.resetRestore();
}
// We don't need to take care of any StateChangeNotifier observers as they
// will die with the content script. The same goes for the privacy transition
// observer that will die with the docShell when the tab is closed.
}
}

View file

@ -109,6 +109,59 @@ const WINDOW_OPEN_FEATURES_MAP = {
statusbar: "status",
};
// Messages that will be received via the Frame Message Manager.
const MESSAGES = [
// The content script sends us data that has been invalidated and needs to
// be saved to disk.
"SessionStore:update",
// The restoreHistory code has run. This is a good time to run SSTabRestoring.
"SessionStore:restoreHistoryComplete",
// The load for the restoring tab has begun. We update the URL bar at this
// time; if we did it before, the load would overwrite it.
"SessionStore:restoreTabContentStarted",
// All network loads for a restoring tab are done, so we should
// consider restoring another tab in the queue. The document has
// been restored, and forms have been filled. We trigger
// SSTabRestored at this time.
"SessionStore:restoreTabContentComplete",
// The content script encountered an error.
"SessionStore:error",
];
// The list of messages we accept from <xul:browser>s that have no tab
// assigned, or whose windows have gone away. Those are for example the
// ones that preload about:newtab pages, or from browsers where the window
// has just been closed.
const NOTAB_MESSAGES = new Set([
// For a description see above.
"SessionStore:update",
// For a description see above.
"SessionStore:error",
]);
// The list of messages we accept without an "epoch" parameter.
// See getCurrentEpoch() and friends to find out what an "epoch" is.
const NOEPOCH_MESSAGES = new Set([
// For a description see above.
"SessionStore:error",
]);
// The list of messages we want to receive even during the short period after a
// frame has been removed from the DOM and before its frame script has finished
// unloading.
const CLOSED_MESSAGES = new Set([
// For a description see above.
"SessionStore:update",
// For a description see above.
"SessionStore:error",
]);
// These are tab events that we listen to.
const TAB_EVENTS = [
"TabOpen",
@ -592,6 +645,10 @@ export var SessionStore = {
SessionStoreInternal.deleteCustomGlobalValue(aKey);
},
persistTabAttribute: function ss_persistTabAttribute(aName) {
SessionStoreInternal.persistTabAttribute(aName);
},
restoreLastSession: function ss_restoreLastSession() {
SessionStoreInternal.restoreLastSession();
},
@ -755,6 +812,18 @@ export var SessionStore = {
}
},
/**
* Prepares to change the remoteness of the given browser, by ensuring that
* the local instance of session history is up-to-date.
*/
async prepareToChangeRemoteness(aTab) {
await SessionStoreInternal.prepareToChangeRemoteness(aTab);
},
finishTabRemotenessChange(aTab, aSwitchId) {
SessionStoreInternal.finishTabRemotenessChange(aTab, aSwitchId);
},
/**
* Clear session store data for a given private browsing window.
* @param {ChromeWindow} win - Open private browsing window to clear data for.
@ -1285,6 +1354,8 @@ var SessionStoreInternal = {
"privacy.resistFingerprinting"
);
Services.prefs.addObserver("privacy.resistFingerprinting", this);
this._shistoryInParent = Services.appinfo.sessionHistoryInParent;
},
/**
@ -1363,26 +1434,33 @@ 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 (Services.appinfo.sessionHistoryInParent) {
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);
}
}
break;
case "browsing-context-discarded":
let permanentKey = aSubject?.embedderElement?.permanentKey;
if (permanentKey) {
this._browserSHistoryListener.get(permanentKey)?.unregister();
if (Services.appinfo.sessionHistoryInParent) {
let permanentKey = aSubject?.embedderElement?.permanentKey;
if (permanentKey) {
this._browserSHistoryListener.get(permanentKey)?.unregister();
}
}
break;
case "browser-shutdown-tabstate-updated":
this.onFinalTabStateUpdateComplete(aSubject);
if (Services.appinfo.sessionHistoryInParent) {
// Non-SHIP code calls this when the frame script is unloaded.
this.onFinalTabStateUpdateComplete(aSubject);
}
this._notifyOfClosedObjectsChange();
break;
}
@ -1495,6 +1573,10 @@ var SessionStoreInternal = {
}
}
if (!Services.appinfo.sessionHistoryInParent) {
throw new Error("This function should only be used with SHIP");
}
if (!permanentKey || browsingContext !== browsingContext.top) {
return null;
}
@ -1609,27 +1691,29 @@ var SessionStoreInternal = {
return;
}
let listener = this.getOrCreateSHistoryListener(
permanentKey,
browsingContext
);
if (Services.appinfo.sessionHistoryInParent) {
let listener = this.getOrCreateSHistoryListener(
permanentKey,
browsingContext
);
if (listener) {
let historychange =
// If it is not the scheduled update (tab closed, window closed etc),
// try to store the loading non-web-controlled page opened in _blank
// first.
(forStorage &&
lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
browsingContext
)) ||
listener.collect(permanentKey, browsingContext, {
collectFull: !!update.sHistoryNeeded,
writeToCache: false,
});
if (listener) {
let historychange =
// If it is not the scheduled update (tab closed, window closed etc),
// try to store the loading non-web-controlled page opened in _blank
// first.
(forStorage &&
lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
browsingContext
)) ||
listener.collect(permanentKey, browsingContext, {
collectFull: !!update.sHistoryNeeded,
writeToCache: false,
});
if (historychange) {
update.data.historychange = historychange;
if (historychange) {
update.data.historychange = historychange;
}
}
}
@ -1640,6 +1724,98 @@ var SessionStoreInternal = {
this.onTabStateUpdate(permanentKey, win, update);
},
/**
* This method handles incoming messages sent by the session store content
* script via the Frame Message Manager or Parent Process Message Manager,
* and thus enables communication with OOP tabs.
*/
receiveMessage(aMessage) {
if (Services.appinfo.sessionHistoryInParent) {
throw new Error(
`received unexpected message '${aMessage.name}' with ` +
`sessionHistoryInParent enabled`
);
}
// If we got here, that means we're dealing with a frame message
// manager message, so the target will be a <xul:browser>.
var browser = aMessage.target;
let win = browser.ownerGlobal;
let tab = win ? win.gBrowser.getTabForBrowser(browser) : null;
// Ensure we receive only specific messages from <xul:browser>s that
// have no tab or window assigned, e.g. the ones that preload
// about:newtab pages, or windows that have closed.
if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
throw new Error(
`received unexpected message '${aMessage.name}' ` +
`from a browser that has no tab or window`
);
}
let data = aMessage.data || {};
let hasEpoch = data.hasOwnProperty("epoch");
// Most messages sent by frame scripts require to pass an epoch.
if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) {
throw new Error(`received message '${aMessage.name}' without an epoch`);
}
// Ignore messages from previous epochs.
if (hasEpoch && !this.isCurrentEpoch(browser.permanentKey, data.epoch)) {
return;
}
switch (aMessage.name) {
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
// frameLoader we know about to compare.
let frameLoader =
browser.frameLoader ||
this._lastKnownFrameLoader.get(browser.permanentKey);
// If the message isn't targeting the latest frameLoader discard it.
if (frameLoader != aMessage.targetFrameLoader) {
return;
}
this.onTabStateUpdate(browser.permanentKey, browser.ownerGlobal, data);
// SHIP code will call this when it receives "browser-shutdown-tabstate-updated"
if (data.isFinal) {
if (!Services.appinfo.sessionHistoryInParent) {
this.onFinalTabStateUpdateComplete(browser);
}
} else if (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
// consumer that's waiting for the flush to be done.
lazy.TabStateFlusher.resolve(browser, data.flushID);
}
break;
case "SessionStore:restoreHistoryComplete":
this._restoreHistoryComplete(browser);
break;
case "SessionStore:restoreTabContentStarted":
this._restoreTabContentStarted(browser, data);
break;
case "SessionStore:restoreTabContentComplete":
this._restoreTabContentComplete(browser, data);
break;
case "SessionStore:error":
lazy.TabStateFlusher.resolveAll(
browser,
false,
"Received error from the content process"
);
break;
default:
throw new Error(`received unknown message '${aMessage.name}'`);
}
},
/* ........ Window Event Handlers .............. */
/**
@ -1741,6 +1917,21 @@ var SessionStoreInternal = {
// internal data about the window.
aWindow.__SSi = this._generateWindowID();
if (!Services.appinfo.sessionHistoryInParent) {
let mm = aWindow.getGroupMessageManager("browsers");
MESSAGES.forEach(msg => {
let listenWhenClosed = CLOSED_MESSAGES.has(msg);
mm.addMessageListener(msg, this, listenWhenClosed);
});
// Load the frame script after registering listeners.
mm.loadFrameScript(
"chrome://browser/content/content-sessionStore.js",
true,
true
);
}
// and create its data object
this._windows[aWindow.__SSi] = {
tabs: [],
@ -2211,6 +2402,11 @@ var SessionStoreInternal = {
// Cache the window state until it is completely gone.
DyingWindowCache.set(aWindow, winData);
if (!Services.appinfo.sessionHistoryInParent) {
let mm = aWindow.getGroupMessageManager("browsers");
MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
}
this._saveableClosedWindowData.delete(winData);
delete aWindow.__SSi;
},
@ -4077,6 +4273,12 @@ var SessionStoreInternal = {
this.saveStateDelayed();
},
persistTabAttribute: function ssi_persistTabAttribute(aName) {
if (lazy.TabAttributes.persist(aName)) {
this.saveStateDelayed();
}
},
/**
* Undoes the closing of a tab or window which corresponds
* to the closedId passed in.
@ -5278,6 +5480,13 @@ var SessionStoreInternal = {
tab.updateLastAccessed(tabData.lastAccessed);
}
if ("attributes" in tabData) {
// Ensure that we persist tab attributes restored from previous sessions.
Object.keys(tabData.attributes).forEach(a =>
lazy.TabAttributes.persist(a)
);
}
if (!tabData.entries) {
tabData.entries = [];
}
@ -5447,6 +5656,7 @@ var SessionStoreInternal = {
let browser = aTab.linkedBrowser;
let window = aTab.ownerGlobal;
let tabbrowser = window.gBrowser;
let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
let activeIndex = tabData.index - 1;
let activePageData = tabData.entries[activeIndex] || null;
@ -5454,9 +5664,36 @@ var SessionStoreInternal = {
this.markTabAsRestoring(aTab);
let isRemotenessUpdate = aOptions.isRemotenessUpdate;
let explicitlyUpdateRemoteness = !Services.appinfo.sessionHistoryInParent;
// If we aren't already updating the browser's remoteness, check if it's
// necessary.
if (explicitlyUpdateRemoteness && !isRemotenessUpdate) {
isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL(
browser,
uri
);
if (isRemotenessUpdate) {
// We updated the remoteness, so we need to send the history down again.
//
// Start a new epoch to discard all frame script messages relating to a
// previous epoch. All async messages that are still on their way to chrome
// will be ignored and don't override any tab data set when restoring.
let epoch = this.startNextEpoch(browser.permanentKey);
this._sendRestoreHistory(browser, {
tabData,
epoch,
loadArguments,
isRemotenessUpdate,
});
}
}
this._sendRestoreTabContent(browser, {
loadArguments,
isRemotenessUpdate: aOptions.isRemotenessUpdate,
isRemotenessUpdate,
reason:
aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE,
});
@ -6591,8 +6828,10 @@ var SessionStoreInternal = {
// The browser is no longer in any sort of restoring state.
TAB_STATE_FOR_BROWSER.delete(browser);
this._restoreListeners.get(browser.permanentKey)?.unregister();
browser.browsingContext.clearRestoreState();
if (Services.appinfo.sessionHistoryInParent) {
this._restoreListeners.get(browser.permanentKey)?.unregister();
browser.browsingContext.clearRestoreState();
}
aTab.removeAttribute("pending");
@ -6616,6 +6855,9 @@ var SessionStoreInternal = {
return;
}
if (!Services.appinfo.sessionHistoryInParent) {
browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
}
this._resetLocalTabRestoringState(tab);
},
@ -6901,6 +7143,10 @@ var SessionStoreInternal = {
* history restores.
*/
_restoreHistory(browser, data) {
if (!Services.appinfo.sessionHistoryInParent) {
throw new Error("This function should only be used with SHIP");
}
this._tabStateToRestore.set(browser.permanentKey, data);
// In case about:blank isn't done yet.
@ -6992,6 +7238,10 @@ var SessionStoreInternal = {
* history restores.
*/
_restoreTabContent(browser, options = {}) {
if (!Services.appinfo.sessionHistoryInParent) {
throw new Error("This function should only be used with SHIP");
}
this._restoreListeners.get(browser.permanentKey)?.unregister();
this._restoreTabContentStarted(browser, options);
@ -7016,7 +7266,14 @@ var SessionStoreInternal = {
},
_sendRestoreTabContent(browser, options) {
this._restoreTabContent(browser, options);
if (Services.appinfo.sessionHistoryInParent) {
this._restoreTabContent(browser, options);
} else {
browser.messageManager.sendAsyncMessage(
"SessionStore:restoreTabContent",
options
);
}
},
_restoreHistoryComplete(browser) {
@ -7160,12 +7417,68 @@ var SessionStoreInternal = {
delete options.tabData.storage;
}
this._restoreHistory(browser, options);
if (Services.appinfo.sessionHistoryInParent) {
this._restoreHistory(browser, options);
} else {
browser.messageManager.sendAsyncMessage(
"SessionStore:restoreHistory",
options
);
}
if (browser && browser.frameLoader) {
browser.frameLoader.requestEpochUpdate(options.epoch);
}
},
// Flush out session history state so that it can be used to restore the state
// into a new process in `finishTabRemotenessChange`.
//
// NOTE: This codepath is temporary while the Fission Session History rewrite
// is in process, and will be removed & replaced once that rewrite is
// complete. (bug 1645062)
async prepareToChangeRemoteness(aBrowser) {
aBrowser.messageManager.sendAsyncMessage(
"SessionStore:prepareForProcessChange"
);
await lazy.TabStateFlusher.flush(aBrowser);
},
// Handle finishing the remoteness change for a tab by restoring session
// history state into it, and resuming the ongoing network load.
//
// NOTE: This codepath is temporary while the Fission Session History rewrite
// is in process, and will be removed & replaced once that rewrite is
// complete. (bug 1645062)
finishTabRemotenessChange(aTab, aSwitchId) {
let window = aTab.ownerGlobal;
if (!window || !window.__SSi || window.closed) {
return;
}
let tabState = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
let options = {
restoreImmediately: true,
restoreContentReason: RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE,
isRemotenessUpdate: true,
loadArguments: {
redirectLoadSwitchId: aSwitchId,
// As we're resuming a load which has been redirected from another
// process, record the history index which is currently being requested.
// It has to be offset by 1 to get back to native history indices from
// SessionStore history indicies.
redirectHistoryIndex: tabState.requestedIndex - 1,
},
};
// Need to reset restoring tabs.
if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) {
this._resetLocalTabRestoringState(aTab);
}
// Restore the state into the tab.
this.restoreTab(aTab, tabState, options);
},
};
/**

View file

@ -2,13 +2,27 @@
* 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/. */
// Tab attributes which are persisted & restored by SessionStore.
const PERSISTED_ATTRIBUTES = ["customizemode"];
// We never want to directly read or write these attributes.
// 'image' should not be accessed directly but handled by using the
// gBrowser.getIcon()/setIcon() methods.
// 'muted' should not be accessed directly but handled by using the
// tab.linkedBrowser.audioMuted/toggleMuteAudio methods.
// 'pending' is used internal by sessionstore and managed accordingly.
const ATTRIBUTES_TO_SKIP = new Set([
"image",
"muted",
"pending",
"skipbackgroundnotify",
]);
// A set of tab attributes to persist. We will read a given list of tab
// attributes when collecting tab data and will re-set those attributes when
// the given tab data is restored to a new tab.
export var TabAttributes = Object.freeze({
persist(name) {
return TabAttributesInternal.persist(name);
},
get(tab) {
return TabAttributesInternal.get(tab);
},
@ -19,10 +33,21 @@ export var TabAttributes = Object.freeze({
});
var TabAttributesInternal = {
_attrs: new Set(),
persist(name) {
if (this._attrs.has(name) || ATTRIBUTES_TO_SKIP.has(name)) {
return false;
}
this._attrs.add(name);
return true;
},
get(tab) {
let data = {};
for (let name of PERSISTED_ATTRIBUTES) {
for (let name of this._attrs) {
if (tab.hasAttribute(name)) {
data[name] = tab.getAttribute(name);
}
@ -32,11 +57,15 @@ var TabAttributesInternal = {
},
set(tab, data = {}) {
// Clear & Set attributes.
for (let name of PERSISTED_ATTRIBUTES) {
// Clear attributes.
for (let name of this._attrs) {
tab.removeAttribute(name);
if (name in data) {
tab.setAttribute(name, data[name]);
}
// Set attributes.
for (let [name, value] of Object.entries(data)) {
if (!ATTRIBUTES_TO_SKIP.has(name)) {
tab.setAttribute(name, value);
}
}
},

View file

@ -2,6 +2,11 @@
* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
});
/**
* A module that enables async flushes. Updates from frame scripts are
* throttled to be sent only once per second. If an action wants a tab's latest
@ -27,6 +32,23 @@ export var TabStateFlusher = Object.freeze({
return TabStateFlusherInternal.flushWindow(window);
},
/**
* Resolves the flush request with the given flush ID.
*
* @param browser (<xul:browser>)
* The browser for which the flush is being resolved.
* @param flushID (int)
* The ID of the flush that was sent to the browser.
* @param success (bool, optional)
* Whether or not the flush succeeded.
* @param message (string, optional)
* An error message that will be sent to the Console in the
* event that a flush failed.
*/
resolve(browser, flushID, success = true, message = "") {
TabStateFlusherInternal.resolve(browser, flushID, success, message);
},
/**
* Resolves all active flush requests for a given browser. This should be
* used when the content process crashed or the final update message was
@ -47,6 +69,9 @@ export var TabStateFlusher = Object.freeze({
});
var TabStateFlusherInternal = {
// Stores the last request ID.
_lastRequestID: 0,
// A map storing all active requests per browser. A request is a
// triple of a map containing all flush requests, a promise that
// resolve when a request for a browser is canceled, and the
@ -54,6 +79,7 @@ var TabStateFlusherInternal = {
_requests: new WeakMap(),
initEntry(entry) {
entry.perBrowserRequests = new Map();
entry.cancelPromise = new Promise(resolve => {
entry.cancel = resolve;
}).then(result => {
@ -70,6 +96,7 @@ var TabStateFlusherInternal = {
* all the latest data.
*/
flush(browser) {
let id = ++this._lastRequestID;
let nativePromise = Promise.resolve();
if (browser && browser.frameLoader) {
/*
@ -79,6 +106,24 @@ var TabStateFlusherInternal = {
nativePromise = browser.frameLoader.requestTabStateFlush();
}
if (!Services.appinfo.sessionHistoryInParent) {
/*
In the event that we have to trigger a process switch and thus change
browser remoteness, session store needs to register and track the new
browser window loaded and to have message manager listener registered
** before ** TabStateFlusher send "SessionStore:flush" message. This fixes
the race where we send the message before the message listener is
registered for it.
*/
lazy.SessionStore.ensureInitialized(browser.ownerGlobal);
let mm = browser.messageManager;
mm.sendAsyncMessage("SessionStore:flush", {
id,
epoch: lazy.SessionStore.getCurrentEpoch(browser),
});
}
// Retrieve active requests for given browser.
let permanentKey = browser.permanentKey;
let request = this._requests.get(permanentKey);
@ -89,10 +134,22 @@ var TabStateFlusherInternal = {
this._requests.set(permanentKey, request);
}
// It's fine to resolve the request immediately after the native promise
// resolves, since SessionStore will have processed all updates from this
// browser by that point.
return Promise.race([nativePromise, request.cancelPromise]);
// Non-SHIP flushes resolve this after the "SessionStore:update" message. We
// don't use that message for SHIP, so it's fine to resolve the request
// immediately after the native promise resolves, since SessionStore will
// have processed all updates from this browser by that point.
let requestPromise = Promise.resolve();
if (!Services.appinfo.sessionHistoryInParent) {
requestPromise = new Promise(resolve => {
// Store resolve() so that we can resolve the promise later.
request.perBrowserRequests.set(id, resolve);
});
}
return Promise.race([
nativePromise.then(_ => requestPromise),
request.cancelPromise,
]);
},
/**
@ -109,6 +166,41 @@ var TabStateFlusherInternal = {
return Promise.all(promises);
},
/**
* Resolves the flush request with the given flush ID.
*
* @param browser (<xul:browser>)
* The browser for which the flush is being resolved.
* @param flushID (int)
* The ID of the flush that was sent to the browser.
* @param success (bool, optional)
* Whether or not the flush succeeded.
* @param message (string, optional)
* An error message that will be sent to the Console in the
* event that a flush failed.
*/
resolve(browser, flushID, success = true, message = "") {
// Nothing to do if there are no pending flushes for the given browser.
if (!this._requests.has(browser.permanentKey)) {
return;
}
// Retrieve active requests for given browser.
let { perBrowserRequests } = this._requests.get(browser.permanentKey);
if (!perBrowserRequests.has(flushID)) {
return;
}
if (!success) {
console.error("Failed to flush browser: ", message);
}
// Resolve the request with the given id.
let resolve = perBrowserRequests.get(flushID);
perBrowserRequests.delete(flushID);
resolve(success);
},
/**
* Resolves all active flush requests for a given browser. This should be
* used when the content process crashed or the final update message was

View file

@ -0,0 +1,13 @@
/* 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/. */
/* eslint-env mozilla/frame-script */
"use strict";
const { ContentSessionStore } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/ContentSessionStore.sys.mjs"
);
void new ContentSessionStore(this);

View file

@ -5,3 +5,4 @@
browser.jar:
* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js)
content/browser/content-sessionStore.js (content/content-sessionStore.js)

View file

@ -11,6 +11,8 @@ MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
JAR_MANIFESTS += ["jar.mn"]
EXTRA_JS_MODULES.sessionstore = [
"ContentRestore.sys.mjs",
"ContentSessionStore.sys.mjs",
"GlobalState.sys.mjs",
"RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
"RunState.sys.mjs",

View file

@ -316,6 +316,7 @@ support-files = ["file_async_flushes.html"]
run-if = ["crashreporter"]
["browser_async_remove_tab.js"]
skip-if = ["!sessionHistoryInParent"]
["browser_async_window_flushing.js"]
https_first_disabled = true
@ -465,6 +466,7 @@ skip-if = [
["browser_privatetabs.js"]
["browser_purge_shistory.js"]
skip-if = ["!sessionHistoryInParent"] # Bug 1271024
["browser_remoteness_flip_on_restore.js"]
@ -514,6 +516,9 @@ skip-if = [
["browser_scrollPositionsReaderMode.js"]
["browser_send_async_message_oom.js"]
skip-if = ["sessionHistoryInParent"] # Tests that the frame script OOMs, which is unused when SHIP is enabled.
["browser_sessionHistory.js"]
https_first_disabled = true
support-files = ["file_sessionHistory_hashchange.html"]

View file

@ -38,31 +38,61 @@ var state = {
add_task(async function test() {
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
await promiseTabState(tab, state);
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
function compareEntries(i, j, history) {
let e1 = history.getEntryAtIndex(i);
let e2 = history.getEntryAtIndex(j);
function compareEntries(i, j, history) {
let e1 = history.getEntryAtIndex(i);
let e2 = history.getEntryAtIndex(j);
ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
for (let c = 0; c < e1.childCount; c++) {
let c1 = e1.GetChildAt(c);
let c2 = e2.GetChildAt(c);
for (let c = 0; c < e1.childCount; c++) {
let c1 = e1.GetChildAt(c);
let c2 = e2.GetChildAt(c);
ok(
c1.sharesDocumentWith(c2),
`Cousins should share documents. (${i}, ${j}, ${c})`
);
}
}
ok(
c1.sharesDocumentWith(c2),
`Cousins should share documents. (${i}, ${j}, ${c})`
);
let history = docShell.browsingContext.childSessionHistory.legacySHistory;
is(history.count, 2, "history.count");
for (let i = 0; i < history.count; i++) {
for (let j = 0; j < history.count; j++) {
compareEntries(i, j, history);
}
}
});
} else {
function compareEntries(i, j, history) {
let e1 = history.getEntryAtIndex(i);
let e2 = history.getEntryAtIndex(j);
ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
for (let c = 0; c < e1.childCount; c++) {
let c1 = e1.GetChildAt(c);
let c2 = e2.GetChildAt(c);
ok(
c1.sharesDocumentWith(c2),
`Cousins should share documents. (${i}, ${j}, ${c})`
);
}
}
}
let history = tab.linkedBrowser.browsingContext.sessionHistory;
let history = tab.linkedBrowser.browsingContext.sessionHistory;
is(history.count, 2, "history.count");
for (let i = 0; i < history.count; i++) {
for (let j = 0; j < history.count; j++) {
compareEntries(i, j, history);
is(history.count, 2, "history.count");
for (let i = 0; i < history.count; i++) {
for (let j = 0; j < history.count; j++) {
compareEntries(i, j, history);
}
}
}

View file

@ -26,8 +26,14 @@ function test() {
let browser = tab.linkedBrowser;
promiseTabState(tab, tabState).then(() => {
let sessionHistory = browser.browsingContext.sessionHistory;
let entry = sessionHistory.getEntryAtIndex(0);
let entry;
if (!Services.appinfo.sessionHistoryInParent) {
let sessionHistory = browser.sessionHistory;
entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
} else {
let sessionHistory = browser.browsingContext.sessionHistory;
entry = sessionHistory.getEntryAtIndex(0);
}
whenChildCount(entry, 1, function () {
whenChildCount(entry, 2, function () {

View file

@ -26,14 +26,26 @@ function test() {
let browser = tab.linkedBrowser;
promiseTabState(tab, tabState).then(() => {
let sessionHistory = browser.browsingContext.sessionHistory;
let entry = sessionHistory.getEntryAtIndex(0);
let entry;
if (!Services.appinfo.sessionHistoryInParent) {
let sessionHistory = browser.sessionHistory;
entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
} else {
let sessionHistory = browser.browsingContext.sessionHistory;
entry = sessionHistory.getEntryAtIndex(0);
}
whenChildCount(entry, 1, function () {
whenChildCount(entry, 2, function () {
promiseBrowserLoaded(browser).then(() => {
let newSessionHistory = browser.browsingContext.sessionHistory;
let newEntry = newSessionHistory.getEntryAtIndex(0);
let newEntry;
if (!Services.appinfo.sessionHistoryInParent) {
let newSessionHistory = browser.sessionHistory;
newEntry = newSessionHistory.legacySHistory.getEntryAtIndex(0);
} else {
let newSessionHistory = browser.browsingContext.sessionHistory;
newEntry = newSessionHistory.getEntryAtIndex(0);
}
whenChildCount(newEntry, 0, function () {
// Make sure that we reset the state.

View file

@ -44,6 +44,58 @@ add_task(async function test_flush() {
gBrowser.removeTab(tab);
});
add_task(async function test_crash() {
if (Services.appinfo.sessionHistoryInParent) {
// This test relies on frame script message ordering. Since the frame script
// is unused with SHIP, there's no guarantee that we'll crash the frame
// before we've started the flush.
ok(true, "Test relies on frame script message ordering.");
return;
}
// Create new tab.
let tab = BrowserTestUtils.addTab(gBrowser, URL);
gBrowser.selectedTab = tab;
let browser = tab.linkedBrowser;
await promiseBrowserLoaded(browser);
// Flush to empty any queued update messages.
await TabStateFlusher.flush(browser);
// There should be one history entry.
let { entries } = JSON.parse(ss.getTabState(tab));
is(entries.length, 1, "there is a single history entry");
// Click the link to navigate.
await SpecialPowers.spawn(browser, [], async function () {
return new Promise(resolve => {
docShell.chromeEventHandler.addEventListener(
"hashchange",
() => resolve(),
{ once: true, capture: true }
);
// Click the link.
content.document.querySelector("a").click();
});
});
// Crash the browser and flush. Both messages are async and will be sent to
// the content process. The "crash" message makes it first so that we don't
// get a chance to process the flush. The TabStateFlusher however should be
// notified so that the flush still completes.
let promise1 = BrowserTestUtils.crashFrame(browser);
let promise2 = TabStateFlusher.flush(browser);
await Promise.all([promise1, promise2]);
// The pending update should be lost.
({ entries } = JSON.parse(ss.getTabState(tab)));
is(entries.length, 1, "still only one history entry");
// Cleanup.
gBrowser.removeTab(tab);
});
add_task(async function test_remove() {
// Create new tab.
let tab = BrowserTestUtils.addTab(gBrowser, URL);

View file

@ -92,7 +92,15 @@ add_task(async function save_worthy_tabs_remote_final() {
ok(browser.isRemoteBrowser, "browser is still remote");
// Remove the tab before the update arrives.
await promiseRemoveTabAndSessionState(tab);
let promise = promiseRemoveTabAndSessionState(tab);
// With SHIP, we'll do the final tab state update sooner than we did before.
if (!Services.appinfo.sessionHistoryInParent) {
// No tab state worth saving (that we know about yet).
ok(!isValueInClosedData(r), "closed tab not saved");
}
await promise;
// Turns out there is a tab state worth saving.
ok(isValueInClosedData(r), "closed tab saved");
@ -109,7 +117,15 @@ add_task(async function save_worthy_tabs_nonremote_final() {
ok(!browser.isRemoteBrowser, "browser is not remote anymore");
// Remove the tab before the update arrives.
await promiseRemoveTabAndSessionState(tab);
let promise = promiseRemoveTabAndSessionState(tab);
// With SHIP, we'll do the final tab state update sooner than we did before.
if (!Services.appinfo.sessionHistoryInParent) {
// No tab state worth saving (that we know about yet).
ok(!isValueInClosedData(r), "closed tab not saved");
}
await promise;
// Turns out there is a tab state worth saving.
ok(isValueInClosedData(r), "closed tab saved");
@ -135,7 +151,15 @@ add_task(async function dont_save_empty_tabs_final() {
await entryReplaced;
// Remove the tab before the update arrives.
await promiseRemoveTabAndSessionState(tab);
let promise = promiseRemoveTabAndSessionState(tab);
// With SHIP, we'll do the final tab state update sooner than we did before.
if (!Services.appinfo.sessionHistoryInParent) {
// Tab state deemed worth saving (yet).
ok(isValueInClosedData(r), "closed tab saved");
}
await promise;
// Turns out we don't want to save the tab state.
ok(!isValueInClosedData(r), "closed tab not saved");

View file

@ -116,10 +116,17 @@ add_task(async function test_remove_uninteresting_window() {
await SpecialPowers.spawn(browser, [], async function () {
// Epic hackery to make this browser seem suddenly boring.
docShell.setCurrentURIForSessionStore(Services.io.newURI("about:blank"));
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
let { sessionHistory } = docShell.QueryInterface(Ci.nsIWebNavigation);
sessionHistory.legacySHistory.purgeHistory(sessionHistory.count);
}
});
let { sessionHistory } = browser.browsingContext;
sessionHistory.purgeHistory(sessionHistory.count);
if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
let { sessionHistory } = browser.browsingContext;
sessionHistory.purgeHistory(sessionHistory.count);
}
// Once this windowClosed Promise resolves, we should have finished
// the flush and revisited our decision to put this window into

View file

@ -38,68 +38,45 @@ add_task(async function test() {
ok(tab.hasAttribute("muted"), "tab.muted exists");
// Make sure we do not persist 'image' and 'muted' attributes.
ss.persistTabAttribute("image");
ss.persistTabAttribute("muted");
let { attributes } = JSON.parse(ss.getTabState(tab));
ok(!("image" in attributes), "'image' attribute not saved");
ok(!("muted" in attributes), "'muted' attribute not saved");
ok(!("customizemode" in attributes), "'customizemode' attribute not saved");
ok(!("custom" in attributes), "'custom' attribute not saved");
// Test persisting a customizemode attribute.
{
let customizationReady = BrowserTestUtils.waitForEvent(
gNavToolbox,
"customizationready"
);
gCustomizeMode.enter();
await customizationReady;
}
// Test persisting a custom attribute.
tab.setAttribute("custom", "foobar");
ss.persistTabAttribute("custom");
let customizeIcon = gBrowser.getIcon(gBrowser.selectedTab);
({ attributes } = JSON.parse(ss.getTabState(gBrowser.selectedTab)));
ok(!("image" in attributes), "'image' attribute not saved");
is(attributes.customizemode, "true", "'customizemode' attribute is correct");
({ attributes } = JSON.parse(ss.getTabState(tab)));
is(attributes.custom, "foobar", "'custom' attribute is correct");
{
let afterCustomization = BrowserTestUtils.waitForEvent(
gNavToolbox,
"aftercustomization"
);
gCustomizeMode.exit();
await afterCustomization;
}
// Test restoring a customizemode tab.
// Make sure we're backwards compatible and restore old 'image' attributes.
let state = {
entries: [],
attributes: { customizemode: "true", nonpersisted: "true" },
entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
attributes: { custom: "foobaz" },
image: gBrowser.getIcon(tab),
};
// Customize mode doesn't like being restored on top of a non-blank tab.
// For the moment, it appears it isn't possible to restore customizemode onto
// an existing non-blank tab outside of tests, however this may be a latent
// bug if we ever try to do that in the future.
let principal = Services.scriptSecurityManager.createNullPrincipal({});
tab.linkedBrowser.createAboutBlankDocumentViewer(principal, principal);
// Prepare a pending tab waiting to be restored.
let promise = promiseTabRestoring(tab);
ss.setTabState(tab, JSON.stringify(state));
await promise;
ok(tab.hasAttribute("pending"), "tab is pending");
ok(tab.hasAttribute("customizemode"), "tab is in customizemode");
ok(!tab.hasAttribute("nonpersisted"), "tab has no nonpersisted attribute");
is(gBrowser.getIcon(tab), customizeIcon, "tab has correct icon");
is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
ok(!state.attributes.image, "'image' attribute not saved");
// Let the pending tab load.
gBrowser.selectedTab = tab;
await promiseTabRestored(tab);
// Ensure no 'image' or 'pending' attributes are stored.
({ attributes } = JSON.parse(ss.getTabState(tab)));
ok(!("image" in attributes), "'image' attribute not saved");
ok(!("pending" in attributes), "'pending' attribute not saved");
ok(!("nonpersisted" in attributes), "'nonpersisted' attribute not saved");
is(attributes.customizemode, "true", "'customizemode' attribute is correct");
is(attributes.custom, "foobaz", "'custom' attribute is correct");
// Clean up.
gBrowser.removeTab(tab);

View file

@ -39,6 +39,7 @@ async function test_bfcache_telemetry(probeInParent) {
add_task(async () => {
await test_bfcache_telemetry(
Services.prefs.getBoolPref("fission.bfcacheInParent")
Services.appinfo.sessionHistoryInParent &&
Services.prefs.getBoolPref("fission.bfcacheInParent")
);
});

View file

@ -4,18 +4,38 @@ add_task(async function duplicateTab() {
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
let historyID = tab.linkedBrowser.browsingContext.historyID;
let shEntry =
tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
is(shEntry.docshellID.toString(), historyID.toString());
if (!Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
let docshell = content.window.docShell.QueryInterface(
Ci.nsIWebNavigation
);
let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
is(shEntry.docshellID.toString(), docshell.historyID.toString());
});
} else {
let historyID = tab.linkedBrowser.browsingContext.historyID;
let shEntry =
tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
is(shEntry.docshellID.toString(), historyID.toString());
}
let tab2 = gBrowser.duplicateTab(tab);
await BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
historyID = tab2.linkedBrowser.browsingContext.historyID;
shEntry =
tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
is(shEntry.docshellID.toString(), historyID.toString());
if (!Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(tab2.linkedBrowser, [], function () {
let docshell = content.window.docShell.QueryInterface(
Ci.nsIWebNavigation
);
let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
is(shEntry.docshellID.toString(), docshell.historyID.toString());
});
} else {
let historyID = tab2.linkedBrowser.browsingContext.historyID;
let shEntry =
tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
is(shEntry.docshellID.toString(), historyID.toString());
}
BrowserTestUtils.removeTab(tab);
BrowserTestUtils.removeTab(tab2);
@ -27,10 +47,24 @@ add_task(async function contentToChromeNavigate() {
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
let historyID = tab.linkedBrowser.browsingContext.historyID;
let sh = tab.linkedBrowser.browsingContext.sessionHistory;
is(sh.count, 1);
is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
if (!Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
let docshell = content.window.docShell.QueryInterface(
Ci.nsIWebNavigation
);
let sh = docshell.sessionHistory;
is(sh.count, 1);
is(
sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
docshell.historyID.toString()
);
});
} else {
let historyID = tab.linkedBrowser.browsingContext.historyID;
let sh = tab.linkedBrowser.browsingContext.sessionHistory;
is(sh.count, 1);
is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
}
// Force the browser to navigate to the chrome process.
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:config");
@ -40,17 +74,31 @@ add_task(async function contentToChromeNavigate() {
let docShell = tab.linkedBrowser.frameLoader.docShell;
// 'cause we're in the chrome process, we can just directly poke at the shistory.
sh = docShell.browsingContext.sessionHistory;
if (!Services.appinfo.sessionHistoryInParent) {
let sh = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
is(sh.count, 2);
is(
sh.getEntryAtIndex(0).docshellID.toString(),
docShell.historyID.toString()
);
is(
sh.getEntryAtIndex(1).docshellID.toString(),
docShell.historyID.toString()
);
is(sh.count, 2);
is(
sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
docShell.historyID.toString()
);
is(
sh.legacySHistory.getEntryAtIndex(1).docshellID.toString(),
docShell.historyID.toString()
);
} else {
let sh = docShell.browsingContext.sessionHistory;
is(sh.count, 2);
is(
sh.getEntryAtIndex(0).docshellID.toString(),
docShell.historyID.toString()
);
is(
sh.getEntryAtIndex(1).docshellID.toString(),
docShell.historyID.toString()
);
}
BrowserTestUtils.removeTab(tab);
});

View file

@ -25,27 +25,54 @@ add_task(async function check_history_not_persisted() {
browser = tab.linkedBrowser;
await promiseTabState(tab, state);
let sessionHistory = browser.browsingContext.sessionHistory;
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(browser, [], function () {
let sessionHistory =
docShell.browsingContext.childSessionHistory.legacySHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
});
} else {
let sessionHistory = browser.browsingContext.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
}
// Load a new URL into the tab, it should replace the about:blank history entry
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
await promiseBrowserLoaded(browser, false, "about:robots");
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(browser, [], function () {
let sessionHistory =
docShell.browsingContext.childSessionHistory.legacySHistory;
sessionHistory = browser.browsingContext.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:robots",
"Should be the right URL"
);
});
} else {
let sessionHistory = browser.browsingContext.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:robots",
"Should be the right URL"
);
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:robots",
"Should be the right URL"
);
}
// Cleanup.
BrowserTestUtils.removeTab(tab);
@ -72,33 +99,64 @@ add_task(async function check_history_default_persisted() {
tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
browser = tab.linkedBrowser;
await promiseTabState(tab, state);
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(browser, [], function () {
let sessionHistory =
docShell.browsingContext.childSessionHistory.legacySHistory;
let sessionHistory = browser.browsingContext.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
});
} else {
let sessionHistory = browser.browsingContext.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
is(sessionHistory.count, 1, "Should be a single history entry");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
}
// Load a new URL into the tab, it should replace the about:blank history entry
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
await promiseBrowserLoaded(browser, false, "about:robots");
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
await SpecialPowers.spawn(browser, [], function () {
let sessionHistory =
docShell.browsingContext.childSessionHistory.legacySHistory;
sessionHistory = browser.browsingContext.sessionHistory;
is(sessionHistory.count, 2, "Should be two history entries");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
is(
sessionHistory.getEntryAtIndex(1).URI.spec,
"about:robots",
"Should be the right URL"
);
});
} else {
let sessionHistory = browser.browsingContext.sessionHistory;
is(sessionHistory.count, 2, "Should be two history entries");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
is(
sessionHistory.getEntryAtIndex(1).URI.spec,
"about:robots",
"Should be the right URL"
);
is(sessionHistory.count, 2, "Should be two history entries");
is(
sessionHistory.getEntryAtIndex(0).URI.spec,
"about:blank",
"Should be the right URL"
);
is(
sessionHistory.getEntryAtIndex(1).URI.spec,
"about:robots",
"Should be the right URL"
);
}
// Cleanup.
BrowserTestUtils.removeTab(tab);

View file

@ -0,0 +1,75 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
const HISTOGRAM_NAME = "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM";
/**
* Test that an OOM in sendAsyncMessage in a framescript will be reported
* to Telemetry.
*/
add_setup(async function () {
Services.telemetry.canRecordExtended = true;
});
function frameScript() {
// Make send[A]syncMessage("SessionStore:update", ...) simulate OOM.
// Other operations are unaffected.
let mm = docShell.messageManager;
let wrap = function (original) {
return function (name, ...args) {
if (name != "SessionStore:update") {
return original(name, ...args);
}
throw new Components.Exception(
"Simulated OOM",
Cr.NS_ERROR_OUT_OF_MEMORY
);
};
};
mm.sendAsyncMessage = wrap(mm.sendAsyncMessage.bind(mm));
mm.sendSyncMessage = wrap(mm.sendSyncMessage.bind(mm));
}
add_task(async function () {
// Capture original state.
let snapshot = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot();
// Open a browser, configure it to cause OOM.
let newTab = BrowserTestUtils.addTab(gBrowser, "about:robots");
let browser = newTab.linkedBrowser;
await ContentTask.spawn(browser, null, frameScript);
let promiseReported = new Promise(resolve => {
browser.messageManager.addMessageListener("SessionStore:error", resolve);
});
// Attempt to flush. This should fail.
let promiseFlushed = TabStateFlusher.flush(browser);
promiseFlushed.then(success => {
if (success) {
throw new Error("Flush should have failed");
}
});
// The frame script should report an error.
await promiseReported;
// Give us some time to handle that error.
await new Promise(resolve => setTimeout(resolve, 10));
// By now, Telemetry should have been updated.
let snapshot2 = Services.telemetry
.getHistogramById(HISTOGRAM_NAME)
.snapshot();
gBrowser.removeTab(newTab);
Assert.ok(snapshot2.sum > snapshot.sum);
});
add_task(async function cleanup() {
Services.telemetry.canRecordExtended = false;
});

View file

@ -296,9 +296,12 @@ add_task(async function test_slow_subframe_load() {
* Ensure that document wireframes can be persisted when they're enabled.
*/
add_task(async function test_wireframes() {
// Wireframes only works when Fission is enabled.
if (!Services.appinfo.fissionAutostart) {
ok(true, "Skipping test_wireframes when Fission is not enabled.");
// Wireframes only works when Fission and SHIP are enabled.
if (
!Services.appinfo.fissionAutostart ||
!Services.appinfo.sessionHistoryInParent
) {
ok(true, "Skipping test_wireframes when Fission or SHIP is not enabled.");
return;
}

View file

@ -558,9 +558,35 @@ function setPropertyOfFormField(browserContext, selector, propName, newValue) {
}
function promiseOnHistoryReplaceEntry(browser) {
return new Promise(resolve => {
let sessionHistory = browser.browsingContext?.sessionHistory;
if (sessionHistory) {
if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
return new Promise(resolve => {
let sessionHistory = browser.browsingContext?.sessionHistory;
if (sessionHistory) {
var historyListener = {
OnHistoryNewEntry() {},
OnHistoryGotoIndex() {},
OnHistoryPurge() {},
OnHistoryReload() {
return true;
},
OnHistoryReplaceEntry() {
resolve();
},
QueryInterface: ChromeUtils.generateQI([
"nsISHistoryListener",
"nsISupportsWeakReference",
]),
};
sessionHistory.addSHistoryListener(historyListener);
}
});
}
return SpecialPowers.spawn(browser, [], () => {
return new Promise(resolve => {
var historyListener = {
OnHistoryNewEntry() {},
OnHistoryGotoIndex() {},
@ -579,8 +605,13 @@ function promiseOnHistoryReplaceEntry(browser) {
]),
};
sessionHistory.addSHistoryListener(historyListener);
}
var { sessionHistory } = this.docShell.QueryInterface(
Ci.nsIWebNavigation
);
if (sessionHistory) {
sessionHistory.legacySHistory.addSHistoryListener(historyListener);
}
});
});
}

View file

@ -263,6 +263,8 @@
"resource:///modules/policies/ProxyPolicies.jsm": "browser/components/enterprisepolicies/helpers/ProxyPolicies.jsm",
"resource:///modules/policies/WebsiteFilter.jsm": "browser/components/enterprisepolicies/helpers/WebsiteFilter.jsm",
"resource:///modules/policies/schema.jsm": "browser/components/enterprisepolicies/schemas/schema.jsm",
"resource:///modules/sessionstore/ContentRestore.jsm": "browser/components/sessionstore/ContentRestore.jsm",
"resource:///modules/sessionstore/ContentSessionStore.jsm": "browser/components/sessionstore/ContentSessionStore.jsm",
"resource:///modules/sessionstore/GlobalState.jsm": "browser/components/sessionstore/GlobalState.jsm",
"resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm": "browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
"resource:///modules/sessionstore/RunState.jsm": "browser/components/sessionstore/RunState.jsm",