forked from mirrors/gecko-dev
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:
parent
90eb86dbb0
commit
159929cd10
26 changed files with 2101 additions and 180 deletions
|
|
@ -2051,6 +2051,18 @@
|
||||||
// doesn't keep the window alive.
|
// doesn't keep the window alive.
|
||||||
b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
|
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 = {
|
const defaultBrowserAttributes = {
|
||||||
contextmenu: "contentAreaContextMenu",
|
contextmenu: "contentAreaContextMenu",
|
||||||
message: "true",
|
message: "true",
|
||||||
|
|
|
||||||
|
|
@ -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 on this list *might* load when creating the process, as opposed to
|
||||||
// items in the main list, which we expect will always load.
|
// items in the main list, which we expect will always load.
|
||||||
const intermittently_loaded_scripts = {
|
const intermittently_loaded_scripts = {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
||||||
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
|
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
|
||||||
DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
|
DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
|
||||||
|
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||||
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
|
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
|
||||||
});
|
});
|
||||||
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
|
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
|
||||||
|
|
@ -220,6 +221,7 @@ CustomizeMode.prototype = {
|
||||||
gTab = aTab;
|
gTab = aTab;
|
||||||
|
|
||||||
gTab.setAttribute("customizemode", "true");
|
gTab.setAttribute("customizemode", "true");
|
||||||
|
lazy.SessionStore.persistTabAttribute("customizemode");
|
||||||
|
|
||||||
if (gTab.linkedPanel) {
|
if (gTab.linkedPanel) {
|
||||||
gTab.linkedBrowser.stop();
|
gTab.linkedBrowser.stop();
|
||||||
|
|
|
||||||
435
browser/components/sessionstore/ContentRestore.sys.mjs
Normal file
435
browser/components/sessionstore/ContentRestore.sys.mjs
Normal 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
685
browser/components/sessionstore/ContentSessionStore.sys.mjs
Normal file
685
browser/components/sessionstore/ContentSessionStore.sys.mjs
Normal 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -109,6 +109,59 @@ const WINDOW_OPEN_FEATURES_MAP = {
|
||||||
statusbar: "status",
|
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.
|
// These are tab events that we listen to.
|
||||||
const TAB_EVENTS = [
|
const TAB_EVENTS = [
|
||||||
"TabOpen",
|
"TabOpen",
|
||||||
|
|
@ -592,6 +645,10 @@ export var SessionStore = {
|
||||||
SessionStoreInternal.deleteCustomGlobalValue(aKey);
|
SessionStoreInternal.deleteCustomGlobalValue(aKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persistTabAttribute: function ss_persistTabAttribute(aName) {
|
||||||
|
SessionStoreInternal.persistTabAttribute(aName);
|
||||||
|
},
|
||||||
|
|
||||||
restoreLastSession: function ss_restoreLastSession() {
|
restoreLastSession: function ss_restoreLastSession() {
|
||||||
SessionStoreInternal.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.
|
* Clear session store data for a given private browsing window.
|
||||||
* @param {ChromeWindow} win - Open private browsing window to clear data for.
|
* @param {ChromeWindow} win - Open private browsing window to clear data for.
|
||||||
|
|
@ -1285,6 +1354,8 @@ var SessionStoreInternal = {
|
||||||
"privacy.resistFingerprinting"
|
"privacy.resistFingerprinting"
|
||||||
);
|
);
|
||||||
Services.prefs.addObserver("privacy.resistFingerprinting", this);
|
Services.prefs.addObserver("privacy.resistFingerprinting", this);
|
||||||
|
|
||||||
|
this._shistoryInParent = Services.appinfo.sessionHistoryInParent;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1363,6 +1434,7 @@ var SessionStoreInternal = {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "browsing-context-did-set-embedder":
|
case "browsing-context-did-set-embedder":
|
||||||
|
if (Services.appinfo.sessionHistoryInParent) {
|
||||||
if (
|
if (
|
||||||
aSubject &&
|
aSubject &&
|
||||||
aSubject === aSubject.top &&
|
aSubject === aSubject.top &&
|
||||||
|
|
@ -1374,15 +1446,21 @@ var SessionStoreInternal = {
|
||||||
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
||||||
this.getOrCreateSHistoryListener(permanentKey, aSubject, true);
|
this.getOrCreateSHistoryListener(permanentKey, aSubject, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "browsing-context-discarded":
|
case "browsing-context-discarded":
|
||||||
|
if (Services.appinfo.sessionHistoryInParent) {
|
||||||
let permanentKey = aSubject?.embedderElement?.permanentKey;
|
let permanentKey = aSubject?.embedderElement?.permanentKey;
|
||||||
if (permanentKey) {
|
if (permanentKey) {
|
||||||
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "browser-shutdown-tabstate-updated":
|
case "browser-shutdown-tabstate-updated":
|
||||||
|
if (Services.appinfo.sessionHistoryInParent) {
|
||||||
|
// Non-SHIP code calls this when the frame script is unloaded.
|
||||||
this.onFinalTabStateUpdateComplete(aSubject);
|
this.onFinalTabStateUpdateComplete(aSubject);
|
||||||
|
}
|
||||||
this._notifyOfClosedObjectsChange();
|
this._notifyOfClosedObjectsChange();
|
||||||
break;
|
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) {
|
if (!permanentKey || browsingContext !== browsingContext.top) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -1609,6 +1691,7 @@ var SessionStoreInternal = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Services.appinfo.sessionHistoryInParent) {
|
||||||
let listener = this.getOrCreateSHistoryListener(
|
let listener = this.getOrCreateSHistoryListener(
|
||||||
permanentKey,
|
permanentKey,
|
||||||
browsingContext
|
browsingContext
|
||||||
|
|
@ -1632,6 +1715,7 @@ var SessionStoreInternal = {
|
||||||
update.data.historychange = historychange;
|
update.data.historychange = historychange;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let win =
|
let win =
|
||||||
browser?.ownerGlobal ??
|
browser?.ownerGlobal ??
|
||||||
|
|
@ -1640,6 +1724,98 @@ var SessionStoreInternal = {
|
||||||
this.onTabStateUpdate(permanentKey, win, update);
|
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 .............. */
|
/* ........ Window Event Handlers .............. */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1741,6 +1917,21 @@ var SessionStoreInternal = {
|
||||||
// internal data about the window.
|
// internal data about the window.
|
||||||
aWindow.__SSi = this._generateWindowID();
|
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
|
// and create its data object
|
||||||
this._windows[aWindow.__SSi] = {
|
this._windows[aWindow.__SSi] = {
|
||||||
tabs: [],
|
tabs: [],
|
||||||
|
|
@ -2211,6 +2402,11 @@ var SessionStoreInternal = {
|
||||||
// Cache the window state until it is completely gone.
|
// Cache the window state until it is completely gone.
|
||||||
DyingWindowCache.set(aWindow, winData);
|
DyingWindowCache.set(aWindow, winData);
|
||||||
|
|
||||||
|
if (!Services.appinfo.sessionHistoryInParent) {
|
||||||
|
let mm = aWindow.getGroupMessageManager("browsers");
|
||||||
|
MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
|
||||||
|
}
|
||||||
|
|
||||||
this._saveableClosedWindowData.delete(winData);
|
this._saveableClosedWindowData.delete(winData);
|
||||||
delete aWindow.__SSi;
|
delete aWindow.__SSi;
|
||||||
},
|
},
|
||||||
|
|
@ -4077,6 +4273,12 @@ var SessionStoreInternal = {
|
||||||
this.saveStateDelayed();
|
this.saveStateDelayed();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
persistTabAttribute: function ssi_persistTabAttribute(aName) {
|
||||||
|
if (lazy.TabAttributes.persist(aName)) {
|
||||||
|
this.saveStateDelayed();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undoes the closing of a tab or window which corresponds
|
* Undoes the closing of a tab or window which corresponds
|
||||||
* to the closedId passed in.
|
* to the closedId passed in.
|
||||||
|
|
@ -5278,6 +5480,13 @@ var SessionStoreInternal = {
|
||||||
tab.updateLastAccessed(tabData.lastAccessed);
|
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) {
|
if (!tabData.entries) {
|
||||||
tabData.entries = [];
|
tabData.entries = [];
|
||||||
}
|
}
|
||||||
|
|
@ -5447,6 +5656,7 @@ var SessionStoreInternal = {
|
||||||
|
|
||||||
let browser = aTab.linkedBrowser;
|
let browser = aTab.linkedBrowser;
|
||||||
let window = aTab.ownerGlobal;
|
let window = aTab.ownerGlobal;
|
||||||
|
let tabbrowser = window.gBrowser;
|
||||||
let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
|
let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
|
||||||
let activeIndex = tabData.index - 1;
|
let activeIndex = tabData.index - 1;
|
||||||
let activePageData = tabData.entries[activeIndex] || null;
|
let activePageData = tabData.entries[activeIndex] || null;
|
||||||
|
|
@ -5454,9 +5664,36 @@ var SessionStoreInternal = {
|
||||||
|
|
||||||
this.markTabAsRestoring(aTab);
|
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, {
|
this._sendRestoreTabContent(browser, {
|
||||||
loadArguments,
|
loadArguments,
|
||||||
isRemotenessUpdate: aOptions.isRemotenessUpdate,
|
isRemotenessUpdate,
|
||||||
reason:
|
reason:
|
||||||
aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE,
|
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.
|
// The browser is no longer in any sort of restoring state.
|
||||||
TAB_STATE_FOR_BROWSER.delete(browser);
|
TAB_STATE_FOR_BROWSER.delete(browser);
|
||||||
|
|
||||||
|
if (Services.appinfo.sessionHistoryInParent) {
|
||||||
this._restoreListeners.get(browser.permanentKey)?.unregister();
|
this._restoreListeners.get(browser.permanentKey)?.unregister();
|
||||||
browser.browsingContext.clearRestoreState();
|
browser.browsingContext.clearRestoreState();
|
||||||
|
}
|
||||||
|
|
||||||
aTab.removeAttribute("pending");
|
aTab.removeAttribute("pending");
|
||||||
|
|
||||||
|
|
@ -6616,6 +6855,9 @@ var SessionStoreInternal = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Services.appinfo.sessionHistoryInParent) {
|
||||||
|
browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
|
||||||
|
}
|
||||||
this._resetLocalTabRestoringState(tab);
|
this._resetLocalTabRestoringState(tab);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -6901,6 +7143,10 @@ var SessionStoreInternal = {
|
||||||
* history restores.
|
* history restores.
|
||||||
*/
|
*/
|
||||||
_restoreHistory(browser, data) {
|
_restoreHistory(browser, data) {
|
||||||
|
if (!Services.appinfo.sessionHistoryInParent) {
|
||||||
|
throw new Error("This function should only be used with SHIP");
|
||||||
|
}
|
||||||
|
|
||||||
this._tabStateToRestore.set(browser.permanentKey, data);
|
this._tabStateToRestore.set(browser.permanentKey, data);
|
||||||
|
|
||||||
// In case about:blank isn't done yet.
|
// In case about:blank isn't done yet.
|
||||||
|
|
@ -6992,6 +7238,10 @@ var SessionStoreInternal = {
|
||||||
* history restores.
|
* history restores.
|
||||||
*/
|
*/
|
||||||
_restoreTabContent(browser, options = {}) {
|
_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._restoreListeners.get(browser.permanentKey)?.unregister();
|
||||||
|
|
||||||
this._restoreTabContentStarted(browser, options);
|
this._restoreTabContentStarted(browser, options);
|
||||||
|
|
@ -7016,7 +7266,14 @@ var SessionStoreInternal = {
|
||||||
},
|
},
|
||||||
|
|
||||||
_sendRestoreTabContent(browser, options) {
|
_sendRestoreTabContent(browser, options) {
|
||||||
|
if (Services.appinfo.sessionHistoryInParent) {
|
||||||
this._restoreTabContent(browser, options);
|
this._restoreTabContent(browser, options);
|
||||||
|
} else {
|
||||||
|
browser.messageManager.sendAsyncMessage(
|
||||||
|
"SessionStore:restoreTabContent",
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreHistoryComplete(browser) {
|
_restoreHistoryComplete(browser) {
|
||||||
|
|
@ -7160,12 +7417,68 @@ var SessionStoreInternal = {
|
||||||
delete options.tabData.storage;
|
delete options.tabData.storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Services.appinfo.sessionHistoryInParent) {
|
||||||
this._restoreHistory(browser, options);
|
this._restoreHistory(browser, options);
|
||||||
|
} else {
|
||||||
|
browser.messageManager.sendAsyncMessage(
|
||||||
|
"SessionStore:restoreHistory",
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (browser && browser.frameLoader) {
|
if (browser && browser.frameLoader) {
|
||||||
browser.frameLoader.requestEpochUpdate(options.epoch);
|
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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,27 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* 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/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
// Tab attributes which are persisted & restored by SessionStore.
|
// We never want to directly read or write these attributes.
|
||||||
const PERSISTED_ATTRIBUTES = ["customizemode"];
|
// '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
|
// 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
|
// attributes when collecting tab data and will re-set those attributes when
|
||||||
// the given tab data is restored to a new tab.
|
// the given tab data is restored to a new tab.
|
||||||
export var TabAttributes = Object.freeze({
|
export var TabAttributes = Object.freeze({
|
||||||
|
persist(name) {
|
||||||
|
return TabAttributesInternal.persist(name);
|
||||||
|
},
|
||||||
|
|
||||||
get(tab) {
|
get(tab) {
|
||||||
return TabAttributesInternal.get(tab);
|
return TabAttributesInternal.get(tab);
|
||||||
},
|
},
|
||||||
|
|
@ -19,10 +33,21 @@ export var TabAttributes = Object.freeze({
|
||||||
});
|
});
|
||||||
|
|
||||||
var TabAttributesInternal = {
|
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) {
|
get(tab) {
|
||||||
let data = {};
|
let data = {};
|
||||||
|
|
||||||
for (let name of PERSISTED_ATTRIBUTES) {
|
for (let name of this._attrs) {
|
||||||
if (tab.hasAttribute(name)) {
|
if (tab.hasAttribute(name)) {
|
||||||
data[name] = tab.getAttribute(name);
|
data[name] = tab.getAttribute(name);
|
||||||
}
|
}
|
||||||
|
|
@ -32,11 +57,15 @@ var TabAttributesInternal = {
|
||||||
},
|
},
|
||||||
|
|
||||||
set(tab, data = {}) {
|
set(tab, data = {}) {
|
||||||
// Clear & Set attributes.
|
// Clear attributes.
|
||||||
for (let name of PERSISTED_ATTRIBUTES) {
|
for (let name of this._attrs) {
|
||||||
tab.removeAttribute(name);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
* 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/. */
|
* 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
|
* 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
|
* 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);
|
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
|
* Resolves all active flush requests for a given browser. This should be
|
||||||
* used when the content process crashed or the final update message was
|
* used when the content process crashed or the final update message was
|
||||||
|
|
@ -47,6 +69,9 @@ export var TabStateFlusher = Object.freeze({
|
||||||
});
|
});
|
||||||
|
|
||||||
var TabStateFlusherInternal = {
|
var TabStateFlusherInternal = {
|
||||||
|
// Stores the last request ID.
|
||||||
|
_lastRequestID: 0,
|
||||||
|
|
||||||
// A map storing all active requests per browser. A request is a
|
// A map storing all active requests per browser. A request is a
|
||||||
// triple of a map containing all flush requests, a promise that
|
// triple of a map containing all flush requests, a promise that
|
||||||
// resolve when a request for a browser is canceled, and the
|
// resolve when a request for a browser is canceled, and the
|
||||||
|
|
@ -54,6 +79,7 @@ var TabStateFlusherInternal = {
|
||||||
_requests: new WeakMap(),
|
_requests: new WeakMap(),
|
||||||
|
|
||||||
initEntry(entry) {
|
initEntry(entry) {
|
||||||
|
entry.perBrowserRequests = new Map();
|
||||||
entry.cancelPromise = new Promise(resolve => {
|
entry.cancelPromise = new Promise(resolve => {
|
||||||
entry.cancel = resolve;
|
entry.cancel = resolve;
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
|
|
@ -70,6 +96,7 @@ var TabStateFlusherInternal = {
|
||||||
* all the latest data.
|
* all the latest data.
|
||||||
*/
|
*/
|
||||||
flush(browser) {
|
flush(browser) {
|
||||||
|
let id = ++this._lastRequestID;
|
||||||
let nativePromise = Promise.resolve();
|
let nativePromise = Promise.resolve();
|
||||||
if (browser && browser.frameLoader) {
|
if (browser && browser.frameLoader) {
|
||||||
/*
|
/*
|
||||||
|
|
@ -79,6 +106,24 @@ var TabStateFlusherInternal = {
|
||||||
nativePromise = browser.frameLoader.requestTabStateFlush();
|
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.
|
// Retrieve active requests for given browser.
|
||||||
let permanentKey = browser.permanentKey;
|
let permanentKey = browser.permanentKey;
|
||||||
let request = this._requests.get(permanentKey);
|
let request = this._requests.get(permanentKey);
|
||||||
|
|
@ -89,10 +134,22 @@ var TabStateFlusherInternal = {
|
||||||
this._requests.set(permanentKey, request);
|
this._requests.set(permanentKey, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's fine to resolve the request immediately after the native promise
|
// Non-SHIP flushes resolve this after the "SessionStore:update" message. We
|
||||||
// resolves, since SessionStore will have processed all updates from this
|
// don't use that message for SHIP, so it's fine to resolve the request
|
||||||
// browser by that point.
|
// immediately after the native promise resolves, since SessionStore will
|
||||||
return Promise.race([nativePromise, request.cancelPromise]);
|
// 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);
|
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
|
* Resolves all active flush requests for a given browser. This should be
|
||||||
* used when the content process crashed or the final update message was
|
* used when the content process crashed or the final update message was
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -5,3 +5,4 @@
|
||||||
browser.jar:
|
browser.jar:
|
||||||
* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
|
* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
|
||||||
content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js)
|
content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js)
|
||||||
|
content/browser/content-sessionStore.js (content/content-sessionStore.js)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
|
||||||
JAR_MANIFESTS += ["jar.mn"]
|
JAR_MANIFESTS += ["jar.mn"]
|
||||||
|
|
||||||
EXTRA_JS_MODULES.sessionstore = [
|
EXTRA_JS_MODULES.sessionstore = [
|
||||||
|
"ContentRestore.sys.mjs",
|
||||||
|
"ContentSessionStore.sys.mjs",
|
||||||
"GlobalState.sys.mjs",
|
"GlobalState.sys.mjs",
|
||||||
"RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
|
"RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
|
||||||
"RunState.sys.mjs",
|
"RunState.sys.mjs",
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ support-files = ["file_async_flushes.html"]
|
||||||
run-if = ["crashreporter"]
|
run-if = ["crashreporter"]
|
||||||
|
|
||||||
["browser_async_remove_tab.js"]
|
["browser_async_remove_tab.js"]
|
||||||
|
skip-if = ["!sessionHistoryInParent"]
|
||||||
|
|
||||||
["browser_async_window_flushing.js"]
|
["browser_async_window_flushing.js"]
|
||||||
https_first_disabled = true
|
https_first_disabled = true
|
||||||
|
|
@ -465,6 +466,7 @@ skip-if = [
|
||||||
["browser_privatetabs.js"]
|
["browser_privatetabs.js"]
|
||||||
|
|
||||||
["browser_purge_shistory.js"]
|
["browser_purge_shistory.js"]
|
||||||
|
skip-if = ["!sessionHistoryInParent"] # Bug 1271024
|
||||||
|
|
||||||
["browser_remoteness_flip_on_restore.js"]
|
["browser_remoteness_flip_on_restore.js"]
|
||||||
|
|
||||||
|
|
@ -514,6 +516,9 @@ skip-if = [
|
||||||
|
|
||||||
["browser_scrollPositionsReaderMode.js"]
|
["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"]
|
["browser_sessionHistory.js"]
|
||||||
https_first_disabled = true
|
https_first_disabled = true
|
||||||
support-files = ["file_sessionHistory_hashchange.html"]
|
support-files = ["file_sessionHistory_hashchange.html"]
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,36 @@ var state = {
|
||||||
add_task(async function test() {
|
add_task(async function test() {
|
||||||
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||||
await promiseTabState(tab, state);
|
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);
|
||||||
|
|
||||||
|
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 = 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) {
|
function compareEntries(i, j, history) {
|
||||||
let e1 = history.getEntryAtIndex(i);
|
let e1 = history.getEntryAtIndex(i);
|
||||||
let e2 = history.getEntryAtIndex(j);
|
let e2 = history.getEntryAtIndex(j);
|
||||||
|
|
@ -65,6 +94,7 @@ add_task(async function test() {
|
||||||
compareEntries(i, j, history);
|
compareEntries(i, j, history);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ss.setBrowserState(stateBackup);
|
ss.setBrowserState(stateBackup);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,14 @@ function test() {
|
||||||
let browser = tab.linkedBrowser;
|
let browser = tab.linkedBrowser;
|
||||||
|
|
||||||
promiseTabState(tab, tabState).then(() => {
|
promiseTabState(tab, tabState).then(() => {
|
||||||
|
let entry;
|
||||||
|
if (!Services.appinfo.sessionHistoryInParent) {
|
||||||
|
let sessionHistory = browser.sessionHistory;
|
||||||
|
entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||||
|
} else {
|
||||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||||
let entry = sessionHistory.getEntryAtIndex(0);
|
entry = sessionHistory.getEntryAtIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
whenChildCount(entry, 1, function () {
|
whenChildCount(entry, 1, function () {
|
||||||
whenChildCount(entry, 2, function () {
|
whenChildCount(entry, 2, function () {
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,26 @@ function test() {
|
||||||
let browser = tab.linkedBrowser;
|
let browser = tab.linkedBrowser;
|
||||||
|
|
||||||
promiseTabState(tab, tabState).then(() => {
|
promiseTabState(tab, tabState).then(() => {
|
||||||
|
let entry;
|
||||||
|
if (!Services.appinfo.sessionHistoryInParent) {
|
||||||
|
let sessionHistory = browser.sessionHistory;
|
||||||
|
entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||||
|
} else {
|
||||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||||
let entry = sessionHistory.getEntryAtIndex(0);
|
entry = sessionHistory.getEntryAtIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
whenChildCount(entry, 1, function () {
|
whenChildCount(entry, 1, function () {
|
||||||
whenChildCount(entry, 2, function () {
|
whenChildCount(entry, 2, function () {
|
||||||
promiseBrowserLoaded(browser).then(() => {
|
promiseBrowserLoaded(browser).then(() => {
|
||||||
|
let newEntry;
|
||||||
|
if (!Services.appinfo.sessionHistoryInParent) {
|
||||||
|
let newSessionHistory = browser.sessionHistory;
|
||||||
|
newEntry = newSessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||||
|
} else {
|
||||||
let newSessionHistory = browser.browsingContext.sessionHistory;
|
let newSessionHistory = browser.browsingContext.sessionHistory;
|
||||||
let newEntry = newSessionHistory.getEntryAtIndex(0);
|
newEntry = newSessionHistory.getEntryAtIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
whenChildCount(newEntry, 0, function () {
|
whenChildCount(newEntry, 0, function () {
|
||||||
// Make sure that we reset the state.
|
// Make sure that we reset the state.
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,58 @@ add_task(async function test_flush() {
|
||||||
gBrowser.removeTab(tab);
|
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() {
|
add_task(async function test_remove() {
|
||||||
// Create new tab.
|
// Create new tab.
|
||||||
let tab = BrowserTestUtils.addTab(gBrowser, URL);
|
let tab = BrowserTestUtils.addTab(gBrowser, URL);
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,15 @@ add_task(async function save_worthy_tabs_remote_final() {
|
||||||
ok(browser.isRemoteBrowser, "browser is still remote");
|
ok(browser.isRemoteBrowser, "browser is still remote");
|
||||||
|
|
||||||
// Remove the tab before the update arrives.
|
// 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.
|
// Turns out there is a tab state worth saving.
|
||||||
ok(isValueInClosedData(r), "closed tab saved");
|
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");
|
ok(!browser.isRemoteBrowser, "browser is not remote anymore");
|
||||||
|
|
||||||
// Remove the tab before the update arrives.
|
// 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.
|
// Turns out there is a tab state worth saving.
|
||||||
ok(isValueInClosedData(r), "closed tab saved");
|
ok(isValueInClosedData(r), "closed tab saved");
|
||||||
|
|
@ -135,7 +151,15 @@ add_task(async function dont_save_empty_tabs_final() {
|
||||||
await entryReplaced;
|
await entryReplaced;
|
||||||
|
|
||||||
// Remove the tab before the update arrives.
|
// 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.
|
// Turns out we don't want to save the tab state.
|
||||||
ok(!isValueInClosedData(r), "closed tab not saved");
|
ok(!isValueInClosedData(r), "closed tab not saved");
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,17 @@ add_task(async function test_remove_uninteresting_window() {
|
||||||
await SpecialPowers.spawn(browser, [], async function () {
|
await SpecialPowers.spawn(browser, [], async function () {
|
||||||
// Epic hackery to make this browser seem suddenly boring.
|
// Epic hackery to make this browser seem suddenly boring.
|
||||||
docShell.setCurrentURIForSessionStore(Services.io.newURI("about:blank"));
|
docShell.setCurrentURIForSessionStore(Services.io.newURI("about:blank"));
|
||||||
|
|
||||||
|
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||||
|
let { sessionHistory } = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||||
|
sessionHistory.legacySHistory.purgeHistory(sessionHistory.count);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||||
let { sessionHistory } = browser.browsingContext;
|
let { sessionHistory } = browser.browsingContext;
|
||||||
sessionHistory.purgeHistory(sessionHistory.count);
|
sessionHistory.purgeHistory(sessionHistory.count);
|
||||||
|
}
|
||||||
|
|
||||||
// Once this windowClosed Promise resolves, we should have finished
|
// Once this windowClosed Promise resolves, we should have finished
|
||||||
// the flush and revisited our decision to put this window into
|
// the flush and revisited our decision to put this window into
|
||||||
|
|
|
||||||
|
|
@ -38,68 +38,45 @@ add_task(async function test() {
|
||||||
ok(tab.hasAttribute("muted"), "tab.muted exists");
|
ok(tab.hasAttribute("muted"), "tab.muted exists");
|
||||||
|
|
||||||
// Make sure we do not persist 'image' and 'muted' attributes.
|
// Make sure we do not persist 'image' and 'muted' attributes.
|
||||||
|
ss.persistTabAttribute("image");
|
||||||
|
ss.persistTabAttribute("muted");
|
||||||
let { attributes } = JSON.parse(ss.getTabState(tab));
|
let { attributes } = JSON.parse(ss.getTabState(tab));
|
||||||
ok(!("image" in attributes), "'image' attribute not saved");
|
ok(!("image" in attributes), "'image' attribute not saved");
|
||||||
ok(!("muted" in attributes), "'muted' 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.
|
// Test persisting a custom attribute.
|
||||||
{
|
tab.setAttribute("custom", "foobar");
|
||||||
let customizationReady = BrowserTestUtils.waitForEvent(
|
ss.persistTabAttribute("custom");
|
||||||
gNavToolbox,
|
|
||||||
"customizationready"
|
|
||||||
);
|
|
||||||
gCustomizeMode.enter();
|
|
||||||
await customizationReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
let customizeIcon = gBrowser.getIcon(gBrowser.selectedTab);
|
({ attributes } = JSON.parse(ss.getTabState(tab)));
|
||||||
({ attributes } = JSON.parse(ss.getTabState(gBrowser.selectedTab)));
|
is(attributes.custom, "foobar", "'custom' attribute is correct");
|
||||||
ok(!("image" in attributes), "'image' attribute not saved");
|
|
||||||
is(attributes.customizemode, "true", "'customizemode' attribute is correct");
|
|
||||||
|
|
||||||
{
|
// Make sure we're backwards compatible and restore old 'image' attributes.
|
||||||
let afterCustomization = BrowserTestUtils.waitForEvent(
|
|
||||||
gNavToolbox,
|
|
||||||
"aftercustomization"
|
|
||||||
);
|
|
||||||
gCustomizeMode.exit();
|
|
||||||
await afterCustomization;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test restoring a customizemode tab.
|
|
||||||
let state = {
|
let state = {
|
||||||
entries: [],
|
entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
|
||||||
attributes: { customizemode: "true", nonpersisted: "true" },
|
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.
|
// Prepare a pending tab waiting to be restored.
|
||||||
let promise = promiseTabRestoring(tab);
|
let promise = promiseTabRestoring(tab);
|
||||||
ss.setTabState(tab, JSON.stringify(state));
|
ss.setTabState(tab, JSON.stringify(state));
|
||||||
await promise;
|
await promise;
|
||||||
|
|
||||||
ok(tab.hasAttribute("pending"), "tab is pending");
|
ok(tab.hasAttribute("pending"), "tab is pending");
|
||||||
ok(tab.hasAttribute("customizemode"), "tab is in customizemode");
|
is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
|
||||||
ok(!tab.hasAttribute("nonpersisted"), "tab has no nonpersisted attribute");
|
|
||||||
is(gBrowser.getIcon(tab), customizeIcon, "tab has correct icon");
|
|
||||||
ok(!state.attributes.image, "'image' attribute not saved");
|
ok(!state.attributes.image, "'image' attribute not saved");
|
||||||
|
|
||||||
// Let the pending tab load.
|
// Let the pending tab load.
|
||||||
gBrowser.selectedTab = tab;
|
gBrowser.selectedTab = tab;
|
||||||
|
await promiseTabRestored(tab);
|
||||||
|
|
||||||
// Ensure no 'image' or 'pending' attributes are stored.
|
// Ensure no 'image' or 'pending' attributes are stored.
|
||||||
({ attributes } = JSON.parse(ss.getTabState(tab)));
|
({ attributes } = JSON.parse(ss.getTabState(tab)));
|
||||||
ok(!("image" in attributes), "'image' attribute not saved");
|
ok(!("image" in attributes), "'image' attribute not saved");
|
||||||
ok(!("pending" in attributes), "'pending' attribute not saved");
|
ok(!("pending" in attributes), "'pending' attribute not saved");
|
||||||
ok(!("nonpersisted" in attributes), "'nonpersisted' attribute not saved");
|
is(attributes.custom, "foobaz", "'custom' attribute is correct");
|
||||||
is(attributes.customizemode, "true", "'customizemode' attribute is correct");
|
|
||||||
|
|
||||||
// Clean up.
|
// Clean up.
|
||||||
gBrowser.removeTab(tab);
|
gBrowser.removeTab(tab);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ async function test_bfcache_telemetry(probeInParent) {
|
||||||
|
|
||||||
add_task(async () => {
|
add_task(async () => {
|
||||||
await test_bfcache_telemetry(
|
await test_bfcache_telemetry(
|
||||||
|
Services.appinfo.sessionHistoryInParent &&
|
||||||
Services.prefs.getBoolPref("fission.bfcacheInParent")
|
Services.prefs.getBoolPref("fission.bfcacheInParent")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,38 @@ add_task(async function duplicateTab() {
|
||||||
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
|
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
|
||||||
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||||
|
|
||||||
|
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 historyID = tab.linkedBrowser.browsingContext.historyID;
|
||||||
let shEntry =
|
let shEntry =
|
||||||
tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
|
tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
|
||||||
is(shEntry.docshellID.toString(), historyID.toString());
|
is(shEntry.docshellID.toString(), historyID.toString());
|
||||||
|
}
|
||||||
|
|
||||||
let tab2 = gBrowser.duplicateTab(tab);
|
let tab2 = gBrowser.duplicateTab(tab);
|
||||||
await BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
|
await BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
|
||||||
|
|
||||||
historyID = tab2.linkedBrowser.browsingContext.historyID;
|
if (!Services.appinfo.sessionHistoryInParent) {
|
||||||
shEntry =
|
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);
|
tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
|
||||||
is(shEntry.docshellID.toString(), historyID.toString());
|
is(shEntry.docshellID.toString(), historyID.toString());
|
||||||
|
}
|
||||||
|
|
||||||
BrowserTestUtils.removeTab(tab);
|
BrowserTestUtils.removeTab(tab);
|
||||||
BrowserTestUtils.removeTab(tab2);
|
BrowserTestUtils.removeTab(tab2);
|
||||||
|
|
@ -27,10 +47,24 @@ add_task(async function contentToChromeNavigate() {
|
||||||
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
|
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
|
||||||
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||||
|
|
||||||
|
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 historyID = tab.linkedBrowser.browsingContext.historyID;
|
||||||
let sh = tab.linkedBrowser.browsingContext.sessionHistory;
|
let sh = tab.linkedBrowser.browsingContext.sessionHistory;
|
||||||
is(sh.count, 1);
|
is(sh.count, 1);
|
||||||
is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
|
is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
|
||||||
|
}
|
||||||
|
|
||||||
// Force the browser to navigate to the chrome process.
|
// Force the browser to navigate to the chrome process.
|
||||||
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:config");
|
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:config");
|
||||||
|
|
@ -40,7 +74,20 @@ add_task(async function contentToChromeNavigate() {
|
||||||
let docShell = tab.linkedBrowser.frameLoader.docShell;
|
let docShell = tab.linkedBrowser.frameLoader.docShell;
|
||||||
|
|
||||||
// 'cause we're in the chrome process, we can just directly poke at the shistory.
|
// '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.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.count, 2);
|
||||||
is(
|
is(
|
||||||
|
|
@ -51,6 +98,7 @@ add_task(async function contentToChromeNavigate() {
|
||||||
sh.getEntryAtIndex(1).docshellID.toString(),
|
sh.getEntryAtIndex(1).docshellID.toString(),
|
||||||
docShell.historyID.toString()
|
docShell.historyID.toString()
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
BrowserTestUtils.removeTab(tab);
|
BrowserTestUtils.removeTab(tab);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,19 @@ add_task(async function check_history_not_persisted() {
|
||||||
browser = tab.linkedBrowser;
|
browser = tab.linkedBrowser;
|
||||||
await promiseTabState(tab, state);
|
await promiseTabState(tab, state);
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||||
|
|
||||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||||
|
|
@ -33,12 +46,15 @@ add_task(async function check_history_not_persisted() {
|
||||||
"about:blank",
|
"about:blank",
|
||||||
"Should be the right URL"
|
"Should be the right URL"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Load a new URL into the tab, it should replace the about:blank history entry
|
// Load a new URL into the tab, it should replace the about:blank history entry
|
||||||
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
|
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
|
||||||
await promiseBrowserLoaded(browser, false, "about:robots");
|
await promiseBrowserLoaded(browser, false, "about:robots");
|
||||||
|
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||||
sessionHistory = browser.browsingContext.sessionHistory;
|
await SpecialPowers.spawn(browser, [], function () {
|
||||||
|
let sessionHistory =
|
||||||
|
docShell.browsingContext.childSessionHistory.legacySHistory;
|
||||||
|
|
||||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||||
is(
|
is(
|
||||||
|
|
@ -46,6 +62,17 @@ add_task(async function check_history_not_persisted() {
|
||||||
"about:robots",
|
"about:robots",
|
||||||
"Should be the right URL"
|
"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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup.
|
// Cleanup.
|
||||||
BrowserTestUtils.removeTab(tab);
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
|
@ -72,7 +99,19 @@ add_task(async function check_history_default_persisted() {
|
||||||
tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||||
browser = tab.linkedBrowser;
|
browser = tab.linkedBrowser;
|
||||||
await promiseTabState(tab, state);
|
await promiseTabState(tab, state);
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||||
|
|
||||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||||
|
|
@ -81,12 +120,15 @@ add_task(async function check_history_default_persisted() {
|
||||||
"about:blank",
|
"about:blank",
|
||||||
"Should be the right URL"
|
"Should be the right URL"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Load a new URL into the tab, it should replace the about:blank history entry
|
// Load a new URL into the tab, it should replace the about:blank history entry
|
||||||
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
|
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
|
||||||
await promiseBrowserLoaded(browser, false, "about:robots");
|
await promiseBrowserLoaded(browser, false, "about:robots");
|
||||||
|
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||||
sessionHistory = browser.browsingContext.sessionHistory;
|
await SpecialPowers.spawn(browser, [], function () {
|
||||||
|
let sessionHistory =
|
||||||
|
docShell.browsingContext.childSessionHistory.legacySHistory;
|
||||||
|
|
||||||
is(sessionHistory.count, 2, "Should be two history entries");
|
is(sessionHistory.count, 2, "Should be two history entries");
|
||||||
is(
|
is(
|
||||||
|
|
@ -99,6 +141,22 @@ add_task(async function check_history_default_persisted() {
|
||||||
"about:robots",
|
"about:robots",
|
||||||
"Should be the right URL"
|
"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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup.
|
// Cleanup.
|
||||||
BrowserTestUtils.removeTab(tab);
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -296,9 +296,12 @@ add_task(async function test_slow_subframe_load() {
|
||||||
* Ensure that document wireframes can be persisted when they're enabled.
|
* Ensure that document wireframes can be persisted when they're enabled.
|
||||||
*/
|
*/
|
||||||
add_task(async function test_wireframes() {
|
add_task(async function test_wireframes() {
|
||||||
// Wireframes only works when Fission is enabled.
|
// Wireframes only works when Fission and SHIP are enabled.
|
||||||
if (!Services.appinfo.fissionAutostart) {
|
if (
|
||||||
ok(true, "Skipping test_wireframes when Fission is not enabled.");
|
!Services.appinfo.fissionAutostart ||
|
||||||
|
!Services.appinfo.sessionHistoryInParent
|
||||||
|
) {
|
||||||
|
ok(true, "Skipping test_wireframes when Fission or SHIP is not enabled.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -558,6 +558,7 @@ function setPropertyOfFormField(browserContext, selector, propName, newValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function promiseOnHistoryReplaceEntry(browser) {
|
function promiseOnHistoryReplaceEntry(browser) {
|
||||||
|
if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
let sessionHistory = browser.browsingContext?.sessionHistory;
|
let sessionHistory = browser.browsingContext?.sessionHistory;
|
||||||
if (sessionHistory) {
|
if (sessionHistory) {
|
||||||
|
|
@ -584,6 +585,36 @@ function promiseOnHistoryReplaceEntry(browser) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return SpecialPowers.spawn(browser, [], () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
var historyListener = {
|
||||||
|
OnHistoryNewEntry() {},
|
||||||
|
OnHistoryGotoIndex() {},
|
||||||
|
OnHistoryPurge() {},
|
||||||
|
OnHistoryReload() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
OnHistoryReplaceEntry() {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
QueryInterface: ChromeUtils.generateQI([
|
||||||
|
"nsISHistoryListener",
|
||||||
|
"nsISupportsWeakReference",
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
var { sessionHistory } = this.docShell.QueryInterface(
|
||||||
|
Ci.nsIWebNavigation
|
||||||
|
);
|
||||||
|
if (sessionHistory) {
|
||||||
|
sessionHistory.legacySHistory.addSHistoryListener(historyListener);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadTestSubscript(filePath) {
|
function loadTestSubscript(filePath) {
|
||||||
Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
|
Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,8 @@
|
||||||
"resource:///modules/policies/ProxyPolicies.jsm": "browser/components/enterprisepolicies/helpers/ProxyPolicies.jsm",
|
"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/WebsiteFilter.jsm": "browser/components/enterprisepolicies/helpers/WebsiteFilter.jsm",
|
||||||
"resource:///modules/policies/schema.jsm": "browser/components/enterprisepolicies/schemas/schema.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/GlobalState.jsm": "browser/components/sessionstore/GlobalState.jsm",
|
||||||
"resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm": "browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
|
"resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm": "browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
|
||||||
"resource:///modules/sessionstore/RunState.jsm": "browser/components/sessionstore/RunState.jsm",
|
"resource:///modules/sessionstore/RunState.jsm": "browser/components/sessionstore/RunState.jsm",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue