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.
|
||||
b.permanentKey = new (Cu.getGlobalForObject(Services).Object)();
|
||||
|
||||
// Ensure that SessionStore has flushed any session history state from the
|
||||
// content process before we this browser's remoteness.
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
b.prepareToChangeRemoteness = () =>
|
||||
SessionStore.prepareToChangeRemoteness(b);
|
||||
b.afterChangeRemoteness = switchId => {
|
||||
let tab = this.getTabForBrowser(b);
|
||||
SessionStore.finishTabRemotenessChange(tab, switchId);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultBrowserAttributes = {
|
||||
contextmenu: "contentAreaContextMenu",
|
||||
message: "true",
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ const known_scripts = {
|
|||
]),
|
||||
};
|
||||
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
known_scripts.modules.add(
|
||||
"resource:///modules/sessionstore/ContentSessionStore.sys.mjs"
|
||||
);
|
||||
}
|
||||
|
||||
// Items on this list *might* load when creating the process, as opposed to
|
||||
// items in the main list, which we expect will always load.
|
||||
const intermittently_loaded_scripts = {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
||||
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
|
||||
DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
|
||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
|
||||
});
|
||||
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
|
||||
|
|
@ -220,6 +221,7 @@ CustomizeMode.prototype = {
|
|||
gTab = aTab;
|
||||
|
||||
gTab.setAttribute("customizemode", "true");
|
||||
lazy.SessionStore.persistTabAttribute("customizemode");
|
||||
|
||||
if (gTab.linkedPanel) {
|
||||
gTab.linkedBrowser.stop();
|
||||
|
|
|
|||
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",
|
||||
};
|
||||
|
||||
// Messages that will be received via the Frame Message Manager.
|
||||
const MESSAGES = [
|
||||
// The content script sends us data that has been invalidated and needs to
|
||||
// be saved to disk.
|
||||
"SessionStore:update",
|
||||
|
||||
// The restoreHistory code has run. This is a good time to run SSTabRestoring.
|
||||
"SessionStore:restoreHistoryComplete",
|
||||
|
||||
// The load for the restoring tab has begun. We update the URL bar at this
|
||||
// time; if we did it before, the load would overwrite it.
|
||||
"SessionStore:restoreTabContentStarted",
|
||||
|
||||
// All network loads for a restoring tab are done, so we should
|
||||
// consider restoring another tab in the queue. The document has
|
||||
// been restored, and forms have been filled. We trigger
|
||||
// SSTabRestored at this time.
|
||||
"SessionStore:restoreTabContentComplete",
|
||||
|
||||
// The content script encountered an error.
|
||||
"SessionStore:error",
|
||||
];
|
||||
|
||||
// The list of messages we accept from <xul:browser>s that have no tab
|
||||
// assigned, or whose windows have gone away. Those are for example the
|
||||
// ones that preload about:newtab pages, or from browsers where the window
|
||||
// has just been closed.
|
||||
const NOTAB_MESSAGES = new Set([
|
||||
// For a description see above.
|
||||
"SessionStore:update",
|
||||
|
||||
// For a description see above.
|
||||
"SessionStore:error",
|
||||
]);
|
||||
|
||||
// The list of messages we accept without an "epoch" parameter.
|
||||
// See getCurrentEpoch() and friends to find out what an "epoch" is.
|
||||
const NOEPOCH_MESSAGES = new Set([
|
||||
// For a description see above.
|
||||
"SessionStore:error",
|
||||
]);
|
||||
|
||||
// The list of messages we want to receive even during the short period after a
|
||||
// frame has been removed from the DOM and before its frame script has finished
|
||||
// unloading.
|
||||
const CLOSED_MESSAGES = new Set([
|
||||
// For a description see above.
|
||||
"SessionStore:update",
|
||||
|
||||
// For a description see above.
|
||||
"SessionStore:error",
|
||||
]);
|
||||
|
||||
// These are tab events that we listen to.
|
||||
const TAB_EVENTS = [
|
||||
"TabOpen",
|
||||
|
|
@ -592,6 +645,10 @@ export var SessionStore = {
|
|||
SessionStoreInternal.deleteCustomGlobalValue(aKey);
|
||||
},
|
||||
|
||||
persistTabAttribute: function ss_persistTabAttribute(aName) {
|
||||
SessionStoreInternal.persistTabAttribute(aName);
|
||||
},
|
||||
|
||||
restoreLastSession: function ss_restoreLastSession() {
|
||||
SessionStoreInternal.restoreLastSession();
|
||||
},
|
||||
|
|
@ -755,6 +812,18 @@ export var SessionStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepares to change the remoteness of the given browser, by ensuring that
|
||||
* the local instance of session history is up-to-date.
|
||||
*/
|
||||
async prepareToChangeRemoteness(aTab) {
|
||||
await SessionStoreInternal.prepareToChangeRemoteness(aTab);
|
||||
},
|
||||
|
||||
finishTabRemotenessChange(aTab, aSwitchId) {
|
||||
SessionStoreInternal.finishTabRemotenessChange(aTab, aSwitchId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear session store data for a given private browsing window.
|
||||
* @param {ChromeWindow} win - Open private browsing window to clear data for.
|
||||
|
|
@ -1285,6 +1354,8 @@ var SessionStoreInternal = {
|
|||
"privacy.resistFingerprinting"
|
||||
);
|
||||
Services.prefs.addObserver("privacy.resistFingerprinting", this);
|
||||
|
||||
this._shistoryInParent = Services.appinfo.sessionHistoryInParent;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -1363,26 +1434,33 @@ var SessionStoreInternal = {
|
|||
}
|
||||
break;
|
||||
case "browsing-context-did-set-embedder":
|
||||
if (
|
||||
aSubject &&
|
||||
aSubject === aSubject.top &&
|
||||
aSubject.isContent &&
|
||||
aSubject.embedderElement &&
|
||||
aSubject.embedderElement.permanentKey
|
||||
) {
|
||||
let permanentKey = aSubject.embedderElement.permanentKey;
|
||||
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
||||
this.getOrCreateSHistoryListener(permanentKey, aSubject, true);
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
if (
|
||||
aSubject &&
|
||||
aSubject === aSubject.top &&
|
||||
aSubject.isContent &&
|
||||
aSubject.embedderElement &&
|
||||
aSubject.embedderElement.permanentKey
|
||||
) {
|
||||
let permanentKey = aSubject.embedderElement.permanentKey;
|
||||
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
||||
this.getOrCreateSHistoryListener(permanentKey, aSubject, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "browsing-context-discarded":
|
||||
let permanentKey = aSubject?.embedderElement?.permanentKey;
|
||||
if (permanentKey) {
|
||||
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
let permanentKey = aSubject?.embedderElement?.permanentKey;
|
||||
if (permanentKey) {
|
||||
this._browserSHistoryListener.get(permanentKey)?.unregister();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "browser-shutdown-tabstate-updated":
|
||||
this.onFinalTabStateUpdateComplete(aSubject);
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
// Non-SHIP code calls this when the frame script is unloaded.
|
||||
this.onFinalTabStateUpdateComplete(aSubject);
|
||||
}
|
||||
this._notifyOfClosedObjectsChange();
|
||||
break;
|
||||
}
|
||||
|
|
@ -1495,6 +1573,10 @@ var SessionStoreInternal = {
|
|||
}
|
||||
}
|
||||
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
throw new Error("This function should only be used with SHIP");
|
||||
}
|
||||
|
||||
if (!permanentKey || browsingContext !== browsingContext.top) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1609,27 +1691,29 @@ var SessionStoreInternal = {
|
|||
return;
|
||||
}
|
||||
|
||||
let listener = this.getOrCreateSHistoryListener(
|
||||
permanentKey,
|
||||
browsingContext
|
||||
);
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
let listener = this.getOrCreateSHistoryListener(
|
||||
permanentKey,
|
||||
browsingContext
|
||||
);
|
||||
|
||||
if (listener) {
|
||||
let historychange =
|
||||
// If it is not the scheduled update (tab closed, window closed etc),
|
||||
// try to store the loading non-web-controlled page opened in _blank
|
||||
// first.
|
||||
(forStorage &&
|
||||
lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
|
||||
browsingContext
|
||||
)) ||
|
||||
listener.collect(permanentKey, browsingContext, {
|
||||
collectFull: !!update.sHistoryNeeded,
|
||||
writeToCache: false,
|
||||
});
|
||||
if (listener) {
|
||||
let historychange =
|
||||
// If it is not the scheduled update (tab closed, window closed etc),
|
||||
// try to store the loading non-web-controlled page opened in _blank
|
||||
// first.
|
||||
(forStorage &&
|
||||
lazy.SessionHistory.collectNonWebControlledBlankLoadingSession(
|
||||
browsingContext
|
||||
)) ||
|
||||
listener.collect(permanentKey, browsingContext, {
|
||||
collectFull: !!update.sHistoryNeeded,
|
||||
writeToCache: false,
|
||||
});
|
||||
|
||||
if (historychange) {
|
||||
update.data.historychange = historychange;
|
||||
if (historychange) {
|
||||
update.data.historychange = historychange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1640,6 +1724,98 @@ var SessionStoreInternal = {
|
|||
this.onTabStateUpdate(permanentKey, win, update);
|
||||
},
|
||||
|
||||
/**
|
||||
* This method handles incoming messages sent by the session store content
|
||||
* script via the Frame Message Manager or Parent Process Message Manager,
|
||||
* and thus enables communication with OOP tabs.
|
||||
*/
|
||||
receiveMessage(aMessage) {
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
throw new Error(
|
||||
`received unexpected message '${aMessage.name}' with ` +
|
||||
`sessionHistoryInParent enabled`
|
||||
);
|
||||
}
|
||||
|
||||
// If we got here, that means we're dealing with a frame message
|
||||
// manager message, so the target will be a <xul:browser>.
|
||||
var browser = aMessage.target;
|
||||
let win = browser.ownerGlobal;
|
||||
let tab = win ? win.gBrowser.getTabForBrowser(browser) : null;
|
||||
|
||||
// Ensure we receive only specific messages from <xul:browser>s that
|
||||
// have no tab or window assigned, e.g. the ones that preload
|
||||
// about:newtab pages, or windows that have closed.
|
||||
if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
|
||||
throw new Error(
|
||||
`received unexpected message '${aMessage.name}' ` +
|
||||
`from a browser that has no tab or window`
|
||||
);
|
||||
}
|
||||
|
||||
let data = aMessage.data || {};
|
||||
let hasEpoch = data.hasOwnProperty("epoch");
|
||||
|
||||
// Most messages sent by frame scripts require to pass an epoch.
|
||||
if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) {
|
||||
throw new Error(`received message '${aMessage.name}' without an epoch`);
|
||||
}
|
||||
|
||||
// Ignore messages from previous epochs.
|
||||
if (hasEpoch && !this.isCurrentEpoch(browser.permanentKey, data.epoch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "SessionStore:update":
|
||||
// |browser.frameLoader| might be empty if the browser was already
|
||||
// destroyed and its tab removed. In that case we still have the last
|
||||
// frameLoader we know about to compare.
|
||||
let frameLoader =
|
||||
browser.frameLoader ||
|
||||
this._lastKnownFrameLoader.get(browser.permanentKey);
|
||||
|
||||
// If the message isn't targeting the latest frameLoader discard it.
|
||||
if (frameLoader != aMessage.targetFrameLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onTabStateUpdate(browser.permanentKey, browser.ownerGlobal, data);
|
||||
|
||||
// SHIP code will call this when it receives "browser-shutdown-tabstate-updated"
|
||||
if (data.isFinal) {
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
this.onFinalTabStateUpdateComplete(browser);
|
||||
}
|
||||
} else if (data.flushID) {
|
||||
// This is an update kicked off by an async flush request. Notify the
|
||||
// TabStateFlusher so that it can finish the request and notify its
|
||||
// consumer that's waiting for the flush to be done.
|
||||
lazy.TabStateFlusher.resolve(browser, data.flushID);
|
||||
}
|
||||
|
||||
break;
|
||||
case "SessionStore:restoreHistoryComplete":
|
||||
this._restoreHistoryComplete(browser);
|
||||
break;
|
||||
case "SessionStore:restoreTabContentStarted":
|
||||
this._restoreTabContentStarted(browser, data);
|
||||
break;
|
||||
case "SessionStore:restoreTabContentComplete":
|
||||
this._restoreTabContentComplete(browser, data);
|
||||
break;
|
||||
case "SessionStore:error":
|
||||
lazy.TabStateFlusher.resolveAll(
|
||||
browser,
|
||||
false,
|
||||
"Received error from the content process"
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`received unknown message '${aMessage.name}'`);
|
||||
}
|
||||
},
|
||||
|
||||
/* ........ Window Event Handlers .............. */
|
||||
|
||||
/**
|
||||
|
|
@ -1741,6 +1917,21 @@ var SessionStoreInternal = {
|
|||
// internal data about the window.
|
||||
aWindow.__SSi = this._generateWindowID();
|
||||
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
let mm = aWindow.getGroupMessageManager("browsers");
|
||||
MESSAGES.forEach(msg => {
|
||||
let listenWhenClosed = CLOSED_MESSAGES.has(msg);
|
||||
mm.addMessageListener(msg, this, listenWhenClosed);
|
||||
});
|
||||
|
||||
// Load the frame script after registering listeners.
|
||||
mm.loadFrameScript(
|
||||
"chrome://browser/content/content-sessionStore.js",
|
||||
true,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// and create its data object
|
||||
this._windows[aWindow.__SSi] = {
|
||||
tabs: [],
|
||||
|
|
@ -2211,6 +2402,11 @@ var SessionStoreInternal = {
|
|||
// Cache the window state until it is completely gone.
|
||||
DyingWindowCache.set(aWindow, winData);
|
||||
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
let mm = aWindow.getGroupMessageManager("browsers");
|
||||
MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
|
||||
}
|
||||
|
||||
this._saveableClosedWindowData.delete(winData);
|
||||
delete aWindow.__SSi;
|
||||
},
|
||||
|
|
@ -4077,6 +4273,12 @@ var SessionStoreInternal = {
|
|||
this.saveStateDelayed();
|
||||
},
|
||||
|
||||
persistTabAttribute: function ssi_persistTabAttribute(aName) {
|
||||
if (lazy.TabAttributes.persist(aName)) {
|
||||
this.saveStateDelayed();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Undoes the closing of a tab or window which corresponds
|
||||
* to the closedId passed in.
|
||||
|
|
@ -5278,6 +5480,13 @@ var SessionStoreInternal = {
|
|||
tab.updateLastAccessed(tabData.lastAccessed);
|
||||
}
|
||||
|
||||
if ("attributes" in tabData) {
|
||||
// Ensure that we persist tab attributes restored from previous sessions.
|
||||
Object.keys(tabData.attributes).forEach(a =>
|
||||
lazy.TabAttributes.persist(a)
|
||||
);
|
||||
}
|
||||
|
||||
if (!tabData.entries) {
|
||||
tabData.entries = [];
|
||||
}
|
||||
|
|
@ -5447,6 +5656,7 @@ var SessionStoreInternal = {
|
|||
|
||||
let browser = aTab.linkedBrowser;
|
||||
let window = aTab.ownerGlobal;
|
||||
let tabbrowser = window.gBrowser;
|
||||
let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
|
||||
let activeIndex = tabData.index - 1;
|
||||
let activePageData = tabData.entries[activeIndex] || null;
|
||||
|
|
@ -5454,9 +5664,36 @@ var SessionStoreInternal = {
|
|||
|
||||
this.markTabAsRestoring(aTab);
|
||||
|
||||
let isRemotenessUpdate = aOptions.isRemotenessUpdate;
|
||||
let explicitlyUpdateRemoteness = !Services.appinfo.sessionHistoryInParent;
|
||||
// If we aren't already updating the browser's remoteness, check if it's
|
||||
// necessary.
|
||||
if (explicitlyUpdateRemoteness && !isRemotenessUpdate) {
|
||||
isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL(
|
||||
browser,
|
||||
uri
|
||||
);
|
||||
|
||||
if (isRemotenessUpdate) {
|
||||
// We updated the remoteness, so we need to send the history down again.
|
||||
//
|
||||
// Start a new epoch to discard all frame script messages relating to a
|
||||
// previous epoch. All async messages that are still on their way to chrome
|
||||
// will be ignored and don't override any tab data set when restoring.
|
||||
let epoch = this.startNextEpoch(browser.permanentKey);
|
||||
|
||||
this._sendRestoreHistory(browser, {
|
||||
tabData,
|
||||
epoch,
|
||||
loadArguments,
|
||||
isRemotenessUpdate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._sendRestoreTabContent(browser, {
|
||||
loadArguments,
|
||||
isRemotenessUpdate: aOptions.isRemotenessUpdate,
|
||||
isRemotenessUpdate,
|
||||
reason:
|
||||
aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE,
|
||||
});
|
||||
|
|
@ -6591,8 +6828,10 @@ var SessionStoreInternal = {
|
|||
// The browser is no longer in any sort of restoring state.
|
||||
TAB_STATE_FOR_BROWSER.delete(browser);
|
||||
|
||||
this._restoreListeners.get(browser.permanentKey)?.unregister();
|
||||
browser.browsingContext.clearRestoreState();
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
this._restoreListeners.get(browser.permanentKey)?.unregister();
|
||||
browser.browsingContext.clearRestoreState();
|
||||
}
|
||||
|
||||
aTab.removeAttribute("pending");
|
||||
|
||||
|
|
@ -6616,6 +6855,9 @@ var SessionStoreInternal = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
|
||||
}
|
||||
this._resetLocalTabRestoringState(tab);
|
||||
},
|
||||
|
||||
|
|
@ -6901,6 +7143,10 @@ var SessionStoreInternal = {
|
|||
* history restores.
|
||||
*/
|
||||
_restoreHistory(browser, data) {
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
throw new Error("This function should only be used with SHIP");
|
||||
}
|
||||
|
||||
this._tabStateToRestore.set(browser.permanentKey, data);
|
||||
|
||||
// In case about:blank isn't done yet.
|
||||
|
|
@ -6992,6 +7238,10 @@ var SessionStoreInternal = {
|
|||
* history restores.
|
||||
*/
|
||||
_restoreTabContent(browser, options = {}) {
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
throw new Error("This function should only be used with SHIP");
|
||||
}
|
||||
|
||||
this._restoreListeners.get(browser.permanentKey)?.unregister();
|
||||
|
||||
this._restoreTabContentStarted(browser, options);
|
||||
|
|
@ -7016,7 +7266,14 @@ var SessionStoreInternal = {
|
|||
},
|
||||
|
||||
_sendRestoreTabContent(browser, options) {
|
||||
this._restoreTabContent(browser, options);
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
this._restoreTabContent(browser, options);
|
||||
} else {
|
||||
browser.messageManager.sendAsyncMessage(
|
||||
"SessionStore:restoreTabContent",
|
||||
options
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_restoreHistoryComplete(browser) {
|
||||
|
|
@ -7160,12 +7417,68 @@ var SessionStoreInternal = {
|
|||
delete options.tabData.storage;
|
||||
}
|
||||
|
||||
this._restoreHistory(browser, options);
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
this._restoreHistory(browser, options);
|
||||
} else {
|
||||
browser.messageManager.sendAsyncMessage(
|
||||
"SessionStore:restoreHistory",
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
if (browser && browser.frameLoader) {
|
||||
browser.frameLoader.requestEpochUpdate(options.epoch);
|
||||
}
|
||||
},
|
||||
|
||||
// Flush out session history state so that it can be used to restore the state
|
||||
// into a new process in `finishTabRemotenessChange`.
|
||||
//
|
||||
// NOTE: This codepath is temporary while the Fission Session History rewrite
|
||||
// is in process, and will be removed & replaced once that rewrite is
|
||||
// complete. (bug 1645062)
|
||||
async prepareToChangeRemoteness(aBrowser) {
|
||||
aBrowser.messageManager.sendAsyncMessage(
|
||||
"SessionStore:prepareForProcessChange"
|
||||
);
|
||||
await lazy.TabStateFlusher.flush(aBrowser);
|
||||
},
|
||||
|
||||
// Handle finishing the remoteness change for a tab by restoring session
|
||||
// history state into it, and resuming the ongoing network load.
|
||||
//
|
||||
// NOTE: This codepath is temporary while the Fission Session History rewrite
|
||||
// is in process, and will be removed & replaced once that rewrite is
|
||||
// complete. (bug 1645062)
|
||||
finishTabRemotenessChange(aTab, aSwitchId) {
|
||||
let window = aTab.ownerGlobal;
|
||||
if (!window || !window.__SSi || window.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tabState = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
|
||||
let options = {
|
||||
restoreImmediately: true,
|
||||
restoreContentReason: RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE,
|
||||
isRemotenessUpdate: true,
|
||||
loadArguments: {
|
||||
redirectLoadSwitchId: aSwitchId,
|
||||
// As we're resuming a load which has been redirected from another
|
||||
// process, record the history index which is currently being requested.
|
||||
// It has to be offset by 1 to get back to native history indices from
|
||||
// SessionStore history indicies.
|
||||
redirectHistoryIndex: tabState.requestedIndex - 1,
|
||||
},
|
||||
};
|
||||
|
||||
// Need to reset restoring tabs.
|
||||
if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) {
|
||||
this._resetLocalTabRestoringState(aTab);
|
||||
}
|
||||
|
||||
// Restore the state into the tab.
|
||||
this.restoreTab(aTab, tabState, options);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,13 +2,27 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Tab attributes which are persisted & restored by SessionStore.
|
||||
const PERSISTED_ATTRIBUTES = ["customizemode"];
|
||||
// We never want to directly read or write these attributes.
|
||||
// 'image' should not be accessed directly but handled by using the
|
||||
// gBrowser.getIcon()/setIcon() methods.
|
||||
// 'muted' should not be accessed directly but handled by using the
|
||||
// tab.linkedBrowser.audioMuted/toggleMuteAudio methods.
|
||||
// 'pending' is used internal by sessionstore and managed accordingly.
|
||||
const ATTRIBUTES_TO_SKIP = new Set([
|
||||
"image",
|
||||
"muted",
|
||||
"pending",
|
||||
"skipbackgroundnotify",
|
||||
]);
|
||||
|
||||
// A set of tab attributes to persist. We will read a given list of tab
|
||||
// attributes when collecting tab data and will re-set those attributes when
|
||||
// the given tab data is restored to a new tab.
|
||||
export var TabAttributes = Object.freeze({
|
||||
persist(name) {
|
||||
return TabAttributesInternal.persist(name);
|
||||
},
|
||||
|
||||
get(tab) {
|
||||
return TabAttributesInternal.get(tab);
|
||||
},
|
||||
|
|
@ -19,10 +33,21 @@ export var TabAttributes = Object.freeze({
|
|||
});
|
||||
|
||||
var TabAttributesInternal = {
|
||||
_attrs: new Set(),
|
||||
|
||||
persist(name) {
|
||||
if (this._attrs.has(name) || ATTRIBUTES_TO_SKIP.has(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._attrs.add(name);
|
||||
return true;
|
||||
},
|
||||
|
||||
get(tab) {
|
||||
let data = {};
|
||||
|
||||
for (let name of PERSISTED_ATTRIBUTES) {
|
||||
for (let name of this._attrs) {
|
||||
if (tab.hasAttribute(name)) {
|
||||
data[name] = tab.getAttribute(name);
|
||||
}
|
||||
|
|
@ -32,11 +57,15 @@ var TabAttributesInternal = {
|
|||
},
|
||||
|
||||
set(tab, data = {}) {
|
||||
// Clear & Set attributes.
|
||||
for (let name of PERSISTED_ATTRIBUTES) {
|
||||
// Clear attributes.
|
||||
for (let name of this._attrs) {
|
||||
tab.removeAttribute(name);
|
||||
if (name in data) {
|
||||
tab.setAttribute(name, data[name]);
|
||||
}
|
||||
|
||||
// Set attributes.
|
||||
for (let [name, value] of Object.entries(data)) {
|
||||
if (!ATTRIBUTES_TO_SKIP.has(name)) {
|
||||
tab.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||
});
|
||||
|
||||
/**
|
||||
* A module that enables async flushes. Updates from frame scripts are
|
||||
* throttled to be sent only once per second. If an action wants a tab's latest
|
||||
|
|
@ -27,6 +32,23 @@ export var TabStateFlusher = Object.freeze({
|
|||
return TabStateFlusherInternal.flushWindow(window);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves the flush request with the given flush ID.
|
||||
*
|
||||
* @param browser (<xul:browser>)
|
||||
* The browser for which the flush is being resolved.
|
||||
* @param flushID (int)
|
||||
* The ID of the flush that was sent to the browser.
|
||||
* @param success (bool, optional)
|
||||
* Whether or not the flush succeeded.
|
||||
* @param message (string, optional)
|
||||
* An error message that will be sent to the Console in the
|
||||
* event that a flush failed.
|
||||
*/
|
||||
resolve(browser, flushID, success = true, message = "") {
|
||||
TabStateFlusherInternal.resolve(browser, flushID, success, message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves all active flush requests for a given browser. This should be
|
||||
* used when the content process crashed or the final update message was
|
||||
|
|
@ -47,6 +69,9 @@ export var TabStateFlusher = Object.freeze({
|
|||
});
|
||||
|
||||
var TabStateFlusherInternal = {
|
||||
// Stores the last request ID.
|
||||
_lastRequestID: 0,
|
||||
|
||||
// A map storing all active requests per browser. A request is a
|
||||
// triple of a map containing all flush requests, a promise that
|
||||
// resolve when a request for a browser is canceled, and the
|
||||
|
|
@ -54,6 +79,7 @@ var TabStateFlusherInternal = {
|
|||
_requests: new WeakMap(),
|
||||
|
||||
initEntry(entry) {
|
||||
entry.perBrowserRequests = new Map();
|
||||
entry.cancelPromise = new Promise(resolve => {
|
||||
entry.cancel = resolve;
|
||||
}).then(result => {
|
||||
|
|
@ -70,6 +96,7 @@ var TabStateFlusherInternal = {
|
|||
* all the latest data.
|
||||
*/
|
||||
flush(browser) {
|
||||
let id = ++this._lastRequestID;
|
||||
let nativePromise = Promise.resolve();
|
||||
if (browser && browser.frameLoader) {
|
||||
/*
|
||||
|
|
@ -79,6 +106,24 @@ var TabStateFlusherInternal = {
|
|||
nativePromise = browser.frameLoader.requestTabStateFlush();
|
||||
}
|
||||
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
/*
|
||||
In the event that we have to trigger a process switch and thus change
|
||||
browser remoteness, session store needs to register and track the new
|
||||
browser window loaded and to have message manager listener registered
|
||||
** before ** TabStateFlusher send "SessionStore:flush" message. This fixes
|
||||
the race where we send the message before the message listener is
|
||||
registered for it.
|
||||
*/
|
||||
lazy.SessionStore.ensureInitialized(browser.ownerGlobal);
|
||||
|
||||
let mm = browser.messageManager;
|
||||
mm.sendAsyncMessage("SessionStore:flush", {
|
||||
id,
|
||||
epoch: lazy.SessionStore.getCurrentEpoch(browser),
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve active requests for given browser.
|
||||
let permanentKey = browser.permanentKey;
|
||||
let request = this._requests.get(permanentKey);
|
||||
|
|
@ -89,10 +134,22 @@ var TabStateFlusherInternal = {
|
|||
this._requests.set(permanentKey, request);
|
||||
}
|
||||
|
||||
// It's fine to resolve the request immediately after the native promise
|
||||
// resolves, since SessionStore will have processed all updates from this
|
||||
// browser by that point.
|
||||
return Promise.race([nativePromise, request.cancelPromise]);
|
||||
// Non-SHIP flushes resolve this after the "SessionStore:update" message. We
|
||||
// don't use that message for SHIP, so it's fine to resolve the request
|
||||
// immediately after the native promise resolves, since SessionStore will
|
||||
// have processed all updates from this browser by that point.
|
||||
let requestPromise = Promise.resolve();
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
requestPromise = new Promise(resolve => {
|
||||
// Store resolve() so that we can resolve the promise later.
|
||||
request.perBrowserRequests.set(id, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.race([
|
||||
nativePromise.then(_ => requestPromise),
|
||||
request.cancelPromise,
|
||||
]);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -109,6 +166,41 @@ var TabStateFlusherInternal = {
|
|||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves the flush request with the given flush ID.
|
||||
*
|
||||
* @param browser (<xul:browser>)
|
||||
* The browser for which the flush is being resolved.
|
||||
* @param flushID (int)
|
||||
* The ID of the flush that was sent to the browser.
|
||||
* @param success (bool, optional)
|
||||
* Whether or not the flush succeeded.
|
||||
* @param message (string, optional)
|
||||
* An error message that will be sent to the Console in the
|
||||
* event that a flush failed.
|
||||
*/
|
||||
resolve(browser, flushID, success = true, message = "") {
|
||||
// Nothing to do if there are no pending flushes for the given browser.
|
||||
if (!this._requests.has(browser.permanentKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve active requests for given browser.
|
||||
let { perBrowserRequests } = this._requests.get(browser.permanentKey);
|
||||
if (!perBrowserRequests.has(flushID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
console.error("Failed to flush browser: ", message);
|
||||
}
|
||||
|
||||
// Resolve the request with the given id.
|
||||
let resolve = perBrowserRequests.get(flushID);
|
||||
perBrowserRequests.delete(flushID);
|
||||
resolve(success);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves all active flush requests for a given browser. This should be
|
||||
* used when the content process crashed or the final update message was
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
* content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml)
|
||||
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"]
|
||||
|
||||
EXTRA_JS_MODULES.sessionstore = [
|
||||
"ContentRestore.sys.mjs",
|
||||
"ContentSessionStore.sys.mjs",
|
||||
"GlobalState.sys.mjs",
|
||||
"RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
|
||||
"RunState.sys.mjs",
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ support-files = ["file_async_flushes.html"]
|
|||
run-if = ["crashreporter"]
|
||||
|
||||
["browser_async_remove_tab.js"]
|
||||
skip-if = ["!sessionHistoryInParent"]
|
||||
|
||||
["browser_async_window_flushing.js"]
|
||||
https_first_disabled = true
|
||||
|
|
@ -465,6 +466,7 @@ skip-if = [
|
|||
["browser_privatetabs.js"]
|
||||
|
||||
["browser_purge_shistory.js"]
|
||||
skip-if = ["!sessionHistoryInParent"] # Bug 1271024
|
||||
|
||||
["browser_remoteness_flip_on_restore.js"]
|
||||
|
||||
|
|
@ -514,6 +516,9 @@ skip-if = [
|
|||
|
||||
["browser_scrollPositionsReaderMode.js"]
|
||||
|
||||
["browser_send_async_message_oom.js"]
|
||||
skip-if = ["sessionHistoryInParent"] # Tests that the frame script OOMs, which is unused when SHIP is enabled.
|
||||
|
||||
["browser_sessionHistory.js"]
|
||||
https_first_disabled = true
|
||||
support-files = ["file_sessionHistory_hashchange.html"]
|
||||
|
|
|
|||
|
|
@ -38,31 +38,61 @@ var state = {
|
|||
add_task(async function test() {
|
||||
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||
await promiseTabState(tab, state);
|
||||
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
|
||||
function compareEntries(i, j, history) {
|
||||
let e1 = history.getEntryAtIndex(i);
|
||||
let e2 = history.getEntryAtIndex(j);
|
||||
|
||||
function compareEntries(i, j, history) {
|
||||
let e1 = history.getEntryAtIndex(i);
|
||||
let e2 = history.getEntryAtIndex(j);
|
||||
ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
|
||||
is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
|
||||
|
||||
ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
|
||||
is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
|
||||
for (let c = 0; c < e1.childCount; c++) {
|
||||
let c1 = e1.GetChildAt(c);
|
||||
let c2 = e2.GetChildAt(c);
|
||||
|
||||
for (let c = 0; c < e1.childCount; c++) {
|
||||
let c1 = e1.GetChildAt(c);
|
||||
let c2 = e2.GetChildAt(c);
|
||||
ok(
|
||||
c1.sharesDocumentWith(c2),
|
||||
`Cousins should share documents. (${i}, ${j}, ${c})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ok(
|
||||
c1.sharesDocumentWith(c2),
|
||||
`Cousins should share documents. (${i}, ${j}, ${c})`
|
||||
);
|
||||
let history = docShell.browsingContext.childSessionHistory.legacySHistory;
|
||||
|
||||
is(history.count, 2, "history.count");
|
||||
for (let i = 0; i < history.count; i++) {
|
||||
for (let j = 0; j < history.count; j++) {
|
||||
compareEntries(i, j, history);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
function compareEntries(i, j, history) {
|
||||
let e1 = history.getEntryAtIndex(i);
|
||||
let e2 = history.getEntryAtIndex(j);
|
||||
|
||||
ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`);
|
||||
is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`);
|
||||
|
||||
for (let c = 0; c < e1.childCount; c++) {
|
||||
let c1 = e1.GetChildAt(c);
|
||||
let c2 = e2.GetChildAt(c);
|
||||
|
||||
ok(
|
||||
c1.sharesDocumentWith(c2),
|
||||
`Cousins should share documents. (${i}, ${j}, ${c})`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let history = tab.linkedBrowser.browsingContext.sessionHistory;
|
||||
let history = tab.linkedBrowser.browsingContext.sessionHistory;
|
||||
|
||||
is(history.count, 2, "history.count");
|
||||
for (let i = 0; i < history.count; i++) {
|
||||
for (let j = 0; j < history.count; j++) {
|
||||
compareEntries(i, j, history);
|
||||
is(history.count, 2, "history.count");
|
||||
for (let i = 0; i < history.count; i++) {
|
||||
for (let j = 0; j < history.count; j++) {
|
||||
compareEntries(i, j, history);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,14 @@ function test() {
|
|||
let browser = tab.linkedBrowser;
|
||||
|
||||
promiseTabState(tab, tabState).then(() => {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
let entry = sessionHistory.getEntryAtIndex(0);
|
||||
let entry;
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
let sessionHistory = browser.sessionHistory;
|
||||
entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||
} else {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
entry = sessionHistory.getEntryAtIndex(0);
|
||||
}
|
||||
|
||||
whenChildCount(entry, 1, function () {
|
||||
whenChildCount(entry, 2, function () {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,26 @@ function test() {
|
|||
let browser = tab.linkedBrowser;
|
||||
|
||||
promiseTabState(tab, tabState).then(() => {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
let entry = sessionHistory.getEntryAtIndex(0);
|
||||
let entry;
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
let sessionHistory = browser.sessionHistory;
|
||||
entry = sessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||
} else {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
entry = sessionHistory.getEntryAtIndex(0);
|
||||
}
|
||||
|
||||
whenChildCount(entry, 1, function () {
|
||||
whenChildCount(entry, 2, function () {
|
||||
promiseBrowserLoaded(browser).then(() => {
|
||||
let newSessionHistory = browser.browsingContext.sessionHistory;
|
||||
let newEntry = newSessionHistory.getEntryAtIndex(0);
|
||||
let newEntry;
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
let newSessionHistory = browser.sessionHistory;
|
||||
newEntry = newSessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||
} else {
|
||||
let newSessionHistory = browser.browsingContext.sessionHistory;
|
||||
newEntry = newSessionHistory.getEntryAtIndex(0);
|
||||
}
|
||||
|
||||
whenChildCount(newEntry, 0, function () {
|
||||
// Make sure that we reset the state.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,58 @@ add_task(async function test_flush() {
|
|||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_crash() {
|
||||
if (Services.appinfo.sessionHistoryInParent) {
|
||||
// This test relies on frame script message ordering. Since the frame script
|
||||
// is unused with SHIP, there's no guarantee that we'll crash the frame
|
||||
// before we've started the flush.
|
||||
ok(true, "Test relies on frame script message ordering.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new tab.
|
||||
let tab = BrowserTestUtils.addTab(gBrowser, URL);
|
||||
gBrowser.selectedTab = tab;
|
||||
let browser = tab.linkedBrowser;
|
||||
await promiseBrowserLoaded(browser);
|
||||
|
||||
// Flush to empty any queued update messages.
|
||||
await TabStateFlusher.flush(browser);
|
||||
|
||||
// There should be one history entry.
|
||||
let { entries } = JSON.parse(ss.getTabState(tab));
|
||||
is(entries.length, 1, "there is a single history entry");
|
||||
|
||||
// Click the link to navigate.
|
||||
await SpecialPowers.spawn(browser, [], async function () {
|
||||
return new Promise(resolve => {
|
||||
docShell.chromeEventHandler.addEventListener(
|
||||
"hashchange",
|
||||
() => resolve(),
|
||||
{ once: true, capture: true }
|
||||
);
|
||||
|
||||
// Click the link.
|
||||
content.document.querySelector("a").click();
|
||||
});
|
||||
});
|
||||
|
||||
// Crash the browser and flush. Both messages are async and will be sent to
|
||||
// the content process. The "crash" message makes it first so that we don't
|
||||
// get a chance to process the flush. The TabStateFlusher however should be
|
||||
// notified so that the flush still completes.
|
||||
let promise1 = BrowserTestUtils.crashFrame(browser);
|
||||
let promise2 = TabStateFlusher.flush(browser);
|
||||
await Promise.all([promise1, promise2]);
|
||||
|
||||
// The pending update should be lost.
|
||||
({ entries } = JSON.parse(ss.getTabState(tab)));
|
||||
is(entries.length, 1, "still only one history entry");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_remove() {
|
||||
// Create new tab.
|
||||
let tab = BrowserTestUtils.addTab(gBrowser, URL);
|
||||
|
|
|
|||
|
|
@ -92,7 +92,15 @@ add_task(async function save_worthy_tabs_remote_final() {
|
|||
ok(browser.isRemoteBrowser, "browser is still remote");
|
||||
|
||||
// Remove the tab before the update arrives.
|
||||
await promiseRemoveTabAndSessionState(tab);
|
||||
let promise = promiseRemoveTabAndSessionState(tab);
|
||||
|
||||
// With SHIP, we'll do the final tab state update sooner than we did before.
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
// No tab state worth saving (that we know about yet).
|
||||
ok(!isValueInClosedData(r), "closed tab not saved");
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
// Turns out there is a tab state worth saving.
|
||||
ok(isValueInClosedData(r), "closed tab saved");
|
||||
|
|
@ -109,7 +117,15 @@ add_task(async function save_worthy_tabs_nonremote_final() {
|
|||
ok(!browser.isRemoteBrowser, "browser is not remote anymore");
|
||||
|
||||
// Remove the tab before the update arrives.
|
||||
await promiseRemoveTabAndSessionState(tab);
|
||||
let promise = promiseRemoveTabAndSessionState(tab);
|
||||
|
||||
// With SHIP, we'll do the final tab state update sooner than we did before.
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
// No tab state worth saving (that we know about yet).
|
||||
ok(!isValueInClosedData(r), "closed tab not saved");
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
// Turns out there is a tab state worth saving.
|
||||
ok(isValueInClosedData(r), "closed tab saved");
|
||||
|
|
@ -135,7 +151,15 @@ add_task(async function dont_save_empty_tabs_final() {
|
|||
await entryReplaced;
|
||||
|
||||
// Remove the tab before the update arrives.
|
||||
await promiseRemoveTabAndSessionState(tab);
|
||||
let promise = promiseRemoveTabAndSessionState(tab);
|
||||
|
||||
// With SHIP, we'll do the final tab state update sooner than we did before.
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
// Tab state deemed worth saving (yet).
|
||||
ok(isValueInClosedData(r), "closed tab saved");
|
||||
}
|
||||
|
||||
await promise;
|
||||
|
||||
// Turns out we don't want to save the tab state.
|
||||
ok(!isValueInClosedData(r), "closed tab not saved");
|
||||
|
|
|
|||
|
|
@ -116,10 +116,17 @@ add_task(async function test_remove_uninteresting_window() {
|
|||
await SpecialPowers.spawn(browser, [], async function () {
|
||||
// Epic hackery to make this browser seem suddenly boring.
|
||||
docShell.setCurrentURIForSessionStore(Services.io.newURI("about:blank"));
|
||||
|
||||
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
let { sessionHistory } = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
sessionHistory.legacySHistory.purgeHistory(sessionHistory.count);
|
||||
}
|
||||
});
|
||||
|
||||
let { sessionHistory } = browser.browsingContext;
|
||||
sessionHistory.purgeHistory(sessionHistory.count);
|
||||
if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
let { sessionHistory } = browser.browsingContext;
|
||||
sessionHistory.purgeHistory(sessionHistory.count);
|
||||
}
|
||||
|
||||
// Once this windowClosed Promise resolves, we should have finished
|
||||
// the flush and revisited our decision to put this window into
|
||||
|
|
|
|||
|
|
@ -38,68 +38,45 @@ add_task(async function test() {
|
|||
ok(tab.hasAttribute("muted"), "tab.muted exists");
|
||||
|
||||
// Make sure we do not persist 'image' and 'muted' attributes.
|
||||
ss.persistTabAttribute("image");
|
||||
ss.persistTabAttribute("muted");
|
||||
let { attributes } = JSON.parse(ss.getTabState(tab));
|
||||
ok(!("image" in attributes), "'image' attribute not saved");
|
||||
ok(!("muted" in attributes), "'muted' attribute not saved");
|
||||
ok(!("customizemode" in attributes), "'customizemode' attribute not saved");
|
||||
ok(!("custom" in attributes), "'custom' attribute not saved");
|
||||
|
||||
// Test persisting a customizemode attribute.
|
||||
{
|
||||
let customizationReady = BrowserTestUtils.waitForEvent(
|
||||
gNavToolbox,
|
||||
"customizationready"
|
||||
);
|
||||
gCustomizeMode.enter();
|
||||
await customizationReady;
|
||||
}
|
||||
// Test persisting a custom attribute.
|
||||
tab.setAttribute("custom", "foobar");
|
||||
ss.persistTabAttribute("custom");
|
||||
|
||||
let customizeIcon = gBrowser.getIcon(gBrowser.selectedTab);
|
||||
({ attributes } = JSON.parse(ss.getTabState(gBrowser.selectedTab)));
|
||||
ok(!("image" in attributes), "'image' attribute not saved");
|
||||
is(attributes.customizemode, "true", "'customizemode' attribute is correct");
|
||||
({ attributes } = JSON.parse(ss.getTabState(tab)));
|
||||
is(attributes.custom, "foobar", "'custom' attribute is correct");
|
||||
|
||||
{
|
||||
let afterCustomization = BrowserTestUtils.waitForEvent(
|
||||
gNavToolbox,
|
||||
"aftercustomization"
|
||||
);
|
||||
gCustomizeMode.exit();
|
||||
await afterCustomization;
|
||||
}
|
||||
|
||||
// Test restoring a customizemode tab.
|
||||
// Make sure we're backwards compatible and restore old 'image' attributes.
|
||||
let state = {
|
||||
entries: [],
|
||||
attributes: { customizemode: "true", nonpersisted: "true" },
|
||||
entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }],
|
||||
attributes: { custom: "foobaz" },
|
||||
image: gBrowser.getIcon(tab),
|
||||
};
|
||||
|
||||
// Customize mode doesn't like being restored on top of a non-blank tab.
|
||||
// For the moment, it appears it isn't possible to restore customizemode onto
|
||||
// an existing non-blank tab outside of tests, however this may be a latent
|
||||
// bug if we ever try to do that in the future.
|
||||
let principal = Services.scriptSecurityManager.createNullPrincipal({});
|
||||
tab.linkedBrowser.createAboutBlankDocumentViewer(principal, principal);
|
||||
|
||||
// Prepare a pending tab waiting to be restored.
|
||||
let promise = promiseTabRestoring(tab);
|
||||
ss.setTabState(tab, JSON.stringify(state));
|
||||
await promise;
|
||||
|
||||
ok(tab.hasAttribute("pending"), "tab is pending");
|
||||
ok(tab.hasAttribute("customizemode"), "tab is in customizemode");
|
||||
ok(!tab.hasAttribute("nonpersisted"), "tab has no nonpersisted attribute");
|
||||
is(gBrowser.getIcon(tab), customizeIcon, "tab has correct icon");
|
||||
is(gBrowser.getIcon(tab), state.image, "tab has correct icon");
|
||||
ok(!state.attributes.image, "'image' attribute not saved");
|
||||
|
||||
// Let the pending tab load.
|
||||
gBrowser.selectedTab = tab;
|
||||
await promiseTabRestored(tab);
|
||||
|
||||
// Ensure no 'image' or 'pending' attributes are stored.
|
||||
({ attributes } = JSON.parse(ss.getTabState(tab)));
|
||||
ok(!("image" in attributes), "'image' attribute not saved");
|
||||
ok(!("pending" in attributes), "'pending' attribute not saved");
|
||||
ok(!("nonpersisted" in attributes), "'nonpersisted' attribute not saved");
|
||||
is(attributes.customizemode, "true", "'customizemode' attribute is correct");
|
||||
is(attributes.custom, "foobaz", "'custom' attribute is correct");
|
||||
|
||||
// Clean up.
|
||||
gBrowser.removeTab(tab);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ async function test_bfcache_telemetry(probeInParent) {
|
|||
|
||||
add_task(async () => {
|
||||
await test_bfcache_telemetry(
|
||||
Services.prefs.getBoolPref("fission.bfcacheInParent")
|
||||
Services.appinfo.sessionHistoryInParent &&
|
||||
Services.prefs.getBoolPref("fission.bfcacheInParent")
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,18 +4,38 @@ add_task(async function duplicateTab() {
|
|||
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
|
||||
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
|
||||
let historyID = tab.linkedBrowser.browsingContext.historyID;
|
||||
let shEntry =
|
||||
tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
|
||||
is(shEntry.docshellID.toString(), historyID.toString());
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
|
||||
let docshell = content.window.docShell.QueryInterface(
|
||||
Ci.nsIWebNavigation
|
||||
);
|
||||
let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||
is(shEntry.docshellID.toString(), docshell.historyID.toString());
|
||||
});
|
||||
} else {
|
||||
let historyID = tab.linkedBrowser.browsingContext.historyID;
|
||||
let shEntry =
|
||||
tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
|
||||
is(shEntry.docshellID.toString(), historyID.toString());
|
||||
}
|
||||
|
||||
let tab2 = gBrowser.duplicateTab(tab);
|
||||
await BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
|
||||
|
||||
historyID = tab2.linkedBrowser.browsingContext.historyID;
|
||||
shEntry =
|
||||
tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
|
||||
is(shEntry.docshellID.toString(), historyID.toString());
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(tab2.linkedBrowser, [], function () {
|
||||
let docshell = content.window.docShell.QueryInterface(
|
||||
Ci.nsIWebNavigation
|
||||
);
|
||||
let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0);
|
||||
is(shEntry.docshellID.toString(), docshell.historyID.toString());
|
||||
});
|
||||
} else {
|
||||
let historyID = tab2.linkedBrowser.browsingContext.historyID;
|
||||
let shEntry =
|
||||
tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0);
|
||||
is(shEntry.docshellID.toString(), historyID.toString());
|
||||
}
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
BrowserTestUtils.removeTab(tab2);
|
||||
|
|
@ -27,10 +47,24 @@ add_task(async function contentToChromeNavigate() {
|
|||
let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
|
||||
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
|
||||
let historyID = tab.linkedBrowser.browsingContext.historyID;
|
||||
let sh = tab.linkedBrowser.browsingContext.sessionHistory;
|
||||
is(sh.count, 1);
|
||||
is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
|
||||
let docshell = content.window.docShell.QueryInterface(
|
||||
Ci.nsIWebNavigation
|
||||
);
|
||||
let sh = docshell.sessionHistory;
|
||||
is(sh.count, 1);
|
||||
is(
|
||||
sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
|
||||
docshell.historyID.toString()
|
||||
);
|
||||
});
|
||||
} else {
|
||||
let historyID = tab.linkedBrowser.browsingContext.historyID;
|
||||
let sh = tab.linkedBrowser.browsingContext.sessionHistory;
|
||||
is(sh.count, 1);
|
||||
is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString());
|
||||
}
|
||||
|
||||
// Force the browser to navigate to the chrome process.
|
||||
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:config");
|
||||
|
|
@ -40,17 +74,31 @@ add_task(async function contentToChromeNavigate() {
|
|||
let docShell = tab.linkedBrowser.frameLoader.docShell;
|
||||
|
||||
// 'cause we're in the chrome process, we can just directly poke at the shistory.
|
||||
sh = docShell.browsingContext.sessionHistory;
|
||||
if (!Services.appinfo.sessionHistoryInParent) {
|
||||
let sh = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
|
||||
|
||||
is(sh.count, 2);
|
||||
is(
|
||||
sh.getEntryAtIndex(0).docshellID.toString(),
|
||||
docShell.historyID.toString()
|
||||
);
|
||||
is(
|
||||
sh.getEntryAtIndex(1).docshellID.toString(),
|
||||
docShell.historyID.toString()
|
||||
);
|
||||
is(sh.count, 2);
|
||||
is(
|
||||
sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(),
|
||||
docShell.historyID.toString()
|
||||
);
|
||||
is(
|
||||
sh.legacySHistory.getEntryAtIndex(1).docshellID.toString(),
|
||||
docShell.historyID.toString()
|
||||
);
|
||||
} else {
|
||||
let sh = docShell.browsingContext.sessionHistory;
|
||||
|
||||
is(sh.count, 2);
|
||||
is(
|
||||
sh.getEntryAtIndex(0).docshellID.toString(),
|
||||
docShell.historyID.toString()
|
||||
);
|
||||
is(
|
||||
sh.getEntryAtIndex(1).docshellID.toString(),
|
||||
docShell.historyID.toString()
|
||||
);
|
||||
}
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,27 +25,54 @@ add_task(async function check_history_not_persisted() {
|
|||
browser = tab.linkedBrowser;
|
||||
await promiseTabState(tab, state);
|
||||
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(browser, [], function () {
|
||||
let sessionHistory =
|
||||
docShell.browsingContext.childSessionHistory.legacySHistory;
|
||||
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
}
|
||||
|
||||
// Load a new URL into the tab, it should replace the about:blank history entry
|
||||
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
|
||||
await promiseBrowserLoaded(browser, false, "about:robots");
|
||||
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(browser, [], function () {
|
||||
let sessionHistory =
|
||||
docShell.browsingContext.childSessionHistory.legacySHistory;
|
||||
|
||||
sessionHistory = browser.browsingContext.sessionHistory;
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:robots",
|
||||
"Should be the right URL"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:robots",
|
||||
"Should be the right URL"
|
||||
);
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:robots",
|
||||
"Should be the right URL"
|
||||
);
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
|
|
@ -72,33 +99,64 @@ add_task(async function check_history_default_persisted() {
|
|||
tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||
browser = tab.linkedBrowser;
|
||||
await promiseTabState(tab, state);
|
||||
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(browser, [], function () {
|
||||
let sessionHistory =
|
||||
docShell.browsingContext.childSessionHistory.legacySHistory;
|
||||
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
is(sessionHistory.count, 1, "Should be a single history entry");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
}
|
||||
|
||||
// Load a new URL into the tab, it should replace the about:blank history entry
|
||||
BrowserTestUtils.startLoadingURIString(browser, "about:robots");
|
||||
await promiseBrowserLoaded(browser, false, "about:robots");
|
||||
if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
await SpecialPowers.spawn(browser, [], function () {
|
||||
let sessionHistory =
|
||||
docShell.browsingContext.childSessionHistory.legacySHistory;
|
||||
|
||||
sessionHistory = browser.browsingContext.sessionHistory;
|
||||
is(sessionHistory.count, 2, "Should be two history entries");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(1).URI.spec,
|
||||
"about:robots",
|
||||
"Should be the right URL"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
let sessionHistory = browser.browsingContext.sessionHistory;
|
||||
|
||||
is(sessionHistory.count, 2, "Should be two history entries");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(1).URI.spec,
|
||||
"about:robots",
|
||||
"Should be the right URL"
|
||||
);
|
||||
is(sessionHistory.count, 2, "Should be two history entries");
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(0).URI.spec,
|
||||
"about:blank",
|
||||
"Should be the right URL"
|
||||
);
|
||||
is(
|
||||
sessionHistory.getEntryAtIndex(1).URI.spec,
|
||||
"about:robots",
|
||||
"Should be the right URL"
|
||||
);
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
add_task(async function test_wireframes() {
|
||||
// Wireframes only works when Fission is enabled.
|
||||
if (!Services.appinfo.fissionAutostart) {
|
||||
ok(true, "Skipping test_wireframes when Fission is not enabled.");
|
||||
// Wireframes only works when Fission and SHIP are enabled.
|
||||
if (
|
||||
!Services.appinfo.fissionAutostart ||
|
||||
!Services.appinfo.sessionHistoryInParent
|
||||
) {
|
||||
ok(true, "Skipping test_wireframes when Fission or SHIP is not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -558,9 +558,35 @@ function setPropertyOfFormField(browserContext, selector, propName, newValue) {
|
|||
}
|
||||
|
||||
function promiseOnHistoryReplaceEntry(browser) {
|
||||
return new Promise(resolve => {
|
||||
let sessionHistory = browser.browsingContext?.sessionHistory;
|
||||
if (sessionHistory) {
|
||||
if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
|
||||
return new Promise(resolve => {
|
||||
let sessionHistory = browser.browsingContext?.sessionHistory;
|
||||
if (sessionHistory) {
|
||||
var historyListener = {
|
||||
OnHistoryNewEntry() {},
|
||||
OnHistoryGotoIndex() {},
|
||||
OnHistoryPurge() {},
|
||||
OnHistoryReload() {
|
||||
return true;
|
||||
},
|
||||
|
||||
OnHistoryReplaceEntry() {
|
||||
resolve();
|
||||
},
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsISHistoryListener",
|
||||
"nsISupportsWeakReference",
|
||||
]),
|
||||
};
|
||||
|
||||
sessionHistory.addSHistoryListener(historyListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return SpecialPowers.spawn(browser, [], () => {
|
||||
return new Promise(resolve => {
|
||||
var historyListener = {
|
||||
OnHistoryNewEntry() {},
|
||||
OnHistoryGotoIndex() {},
|
||||
|
|
@ -579,8 +605,13 @@ function promiseOnHistoryReplaceEntry(browser) {
|
|||
]),
|
||||
};
|
||||
|
||||
sessionHistory.addSHistoryListener(historyListener);
|
||||
}
|
||||
var { sessionHistory } = this.docShell.QueryInterface(
|
||||
Ci.nsIWebNavigation
|
||||
);
|
||||
if (sessionHistory) {
|
||||
sessionHistory.legacySHistory.addSHistoryListener(historyListener);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,8 @@
|
|||
"resource:///modules/policies/ProxyPolicies.jsm": "browser/components/enterprisepolicies/helpers/ProxyPolicies.jsm",
|
||||
"resource:///modules/policies/WebsiteFilter.jsm": "browser/components/enterprisepolicies/helpers/WebsiteFilter.jsm",
|
||||
"resource:///modules/policies/schema.jsm": "browser/components/enterprisepolicies/schemas/schema.jsm",
|
||||
"resource:///modules/sessionstore/ContentRestore.jsm": "browser/components/sessionstore/ContentRestore.jsm",
|
||||
"resource:///modules/sessionstore/ContentSessionStore.jsm": "browser/components/sessionstore/ContentSessionStore.jsm",
|
||||
"resource:///modules/sessionstore/GlobalState.jsm": "browser/components/sessionstore/GlobalState.jsm",
|
||||
"resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm": "browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
|
||||
"resource:///modules/sessionstore/RunState.jsm": "browser/components/sessionstore/RunState.jsm",
|
||||
|
|
|
|||
Loading…
Reference in a new issue