gecko-dev/browser/components/sessionstore/ContentSessionStore.jsm

1042 строки
33 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["ContentSessionStore"];
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
ChromeUtils.import("resource://gre/modules/Timer.jsm", this);
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
function debug(msg) {
Services.console.logStringMessage("SessionStoreContent: " + msg);
}
ChromeUtils.defineModuleGetter(this, "FormData",
"resource://gre/modules/FormData.jsm");
ChromeUtils.defineModuleGetter(this, "ContentRestore",
"resource:///modules/sessionstore/ContentRestore.jsm");
ChromeUtils.defineModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
ChromeUtils.defineModuleGetter(this, "ScrollPosition",
"resource://gre/modules/ScrollPosition.jsm");
ChromeUtils.defineModuleGetter(this, "SessionHistory",
"resource://gre/modules/sessionstore/SessionHistory.jsm");
ChromeUtils.defineModuleGetter(this, "SessionStorage",
"resource:///modules/sessionstore/SessionStorage.jsm");
ChromeUtils.defineModuleGetter(this, "Utils",
"resource://gre/modules/sessionstore/Utils.jsm");
const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"]
.getService(Ci.nsISessionStoreUtils);
// A bound to the size of data to store for DOM Storage.
const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit";
// 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;
/**
* A function that will recursively call |cb| to collect data for all
* non-dynamic frames in the current frame/docShell tree.
*/
function mapFrameTree(mm, callback) {
let [data] = Utils.mapFrameTree(mm.content, callback);
return data;
}
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;
}
get stateChangeNotifier() {
return this.store.stateChangeNotifier;
}
}
/**
* Listens for state change notifcations from webProgress and notifies each
* registered observer for either the start of a page load, or its completion.
*/
class StateChangeNotifier extends Handler {
constructor(store) {
super(store);
this._observers = new Set();
let ifreq = this.mm.docShell.QueryInterface(Ci.nsIInterfaceRequestor);
let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
}
/**
* Adds a given observer |obs| to the set of observers that will be notified
* when when a new document starts or finishes loading.
*
* @param obs (object)
*/
addObserver(obs) {
this._observers.add(obs);
}
/**
* Notifies all observers that implement the given |method|.
*
* @param method (string)
*/
notifyObservers(method) {
for (let obs of this._observers) {
if (typeof obs[method] == "function") {
obs[method]();
}
}
}
/**
* @see nsIWebProgressListener.onStateChange
*/
onStateChange(webProgress, request, stateFlags, status) {
// Ignore state changes for subframes because we're only interested in the
// top-document starting or stopping its load.
if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) {
return;
}
// onStateChange will be fired when loading the initial about:blank URI for
// a browser, which we don't actually care about. This is particularly for
// the case of unrestored background tabs, where the content has not yet
// been restored: we don't want to accidentally send any updates to the
// parent when the about:blank placeholder page has loaded.
if (!this.mm.docShell.hasLoadedNonBlankURI) {
return;
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
this.notifyObservers("onPageLoadStarted");
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
this.notifyObservers("onPageLoadCompleted");
}
}
}
StateChangeNotifier.prototype.QueryInterface =
ChromeUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]);
/**
* Listens for and handles content events that we need for the
* session store service to be notified of state changes in content.
*/
class EventListener extends Handler {
constructor(store) {
super(store);
ssu.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.jsm.
content.addEventListener("AboutReaderContentReady", this);
return;
}
content.removeEventListener("AboutReaderContentReady", this);
}
if (this.contentRestoreInitialized) {
// Restore the form data and scroll position. If we're not currently
// restoring a tab state then this call will simply be a noop.
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;
// The state change observer is needed to handle initial subframe loads.
// It will redundantly invalidate with the SHistoryListener in some cases
// but these invalidations are very cheap.
this.stateChangeNotifier.addObserver(this);
// By adding the SHistoryListener immediately, we will unfortunately be
// notified of every history entry as the tab is restored. We don't bother
// waiting to add the listener later because these notifications are cheap.
// We will likely only collect once since we are batching collection on
// a delay.
this.mm.docShell.QueryInterface(Ci.nsIWebNavigation)
.sessionHistory.legacySHistory.addSHistoryListener(this);
// Collect data if we start with a non-empty shistory.
if (!SessionHistory.isEmpty(this.mm.docShell)) {
this.collect();
// When a tab is detached from the window, for the new window there is a
// new SessionHistoryListener created. Normally it is empty at this point
// but in a test env. the initial about:blank might have a children in which
// case we fire off a history message here with about:blank in it. If we
// don't do it ASAP then there is going to be a browser swap and the parent
// will be all confused by that message.
this.messageQueue.send();
}
// Listen for page title changes.
this.mm.addEventListener("DOMTitleChanged", this);
}
uninit() {
let sessionHistory = this.mm.docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
if (sessionHistory) {
sessionHistory.legacySHistory.removeSHistoryListener(this);
}
}
collect() {
// We want to send down a historychange even for full collects in case our
// session history is a partial session history, in which case we don't have
// enough information for a full update. collectFrom(-1) tells the collect
// function to collect all data avaliable in this process.
if (this.mm.docShell) {
this.collectFrom(-1);
}
}
// History can grow relatively big with the nested elements, so if we don't have to, we
// don't want to send the entire history all the time. For a simple optimization
// we keep track of the smallest index from after any change has occured and we just send
// the elements from that index. If something more complicated happens we just clear it
// and send the entire history. We always send the additional info like the current selected
// index (so for going back and forth between history entries we set the index to kLastIndex
// if nothing else changed send an empty array and the additonal info like the selected index)
collectFrom(idx) {
if (this._fromIdx <= idx) {
// If we already know that we need to update history fromn index N we can ignore any changes
// tha happened with an element with index larger than N.
// Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything
// here, and in case of navigation in the history back and forth we use kLastIndex which ignores
// only the subsequent navigations, but not any new elements added.
return;
}
this._fromIdx = idx;
this.messageQueue.push("historychange", () => {
if (this._fromIdx === kNoIndex) {
return null;
}
let history = SessionHistory.collect(this.mm.docShell, this._fromIdx);
this._fromIdx = kNoIndex;
return history;
});
}
handleEvent(event) {
this.collect();
}
onPageLoadCompleted() {
this.collect();
}
onPageLoadStarted() {
this.collect();
}
OnHistoryNewEntry(newURI, oldIndex) {
// We ought to collect the previously current entry as well, see bug 1350567.
this.collectFrom(oldIndex);
}
OnHistoryGoBack(backURI) {
// We ought to collect the previously current entry as well, see bug 1350567.
this.collectFrom(kLastIndex);
return true;
}
OnHistoryGoForward(forwardURI) {
// We ought to collect the previously current entry as well, see bug 1350567.
this.collectFrom(kLastIndex);
return true;
}
OnHistoryGotoIndex(index, gotoURI) {
// We ought to collect the previously current entry as well, see bug 1350567.
this.collectFrom(kLastIndex);
return true;
}
OnHistoryPurge(numEntries) {
this.collect();
return true;
}
OnHistoryReload(reloadURI, reloadFlags) {
this.collect();
return true;
}
OnHistoryReplaceEntry(index) {
this.collect();
}
OnLengthChanged(aCount) {
// Ignore, the method is implemented so that XPConnect doesn't throw!
}
OnIndexChanged(aIndex) {
// Ignore, the method is implemented so that XPConnect doesn't throw!
}
}
SessionHistoryListener.prototype.QueryInterface =
ChromeUtils.generateQI([Ci.nsISHistoryListener,
Ci.nsISupportsWeakReference]);
/**
* Listens for scroll position changes. Whenever the user scrolls the top-most
* frame we update the scroll position and will restore it when requested.
*
* Causes a SessionStore:update message to be sent that contains the current
* scroll positions as a tree of strings. If no frame of the whole frame tree
* is scrolled this will return null so that we don't tack a property onto
* the tabData object in the parent process.
*
* Example:
* {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
*/
class ScrollPositionListener extends Handler {
constructor(store) {
super(store);
ssu.addDynamicFrameFilteredListener(this.mm, "scroll", this, false);
this.stateChangeNotifier.addObserver(this);
}
handleEvent() {
this.messageQueue.push("scroll", () => this.collect());
}
onPageLoadCompleted() {
this.messageQueue.push("scroll", () => this.collect());
}
onPageLoadStarted() {
this.messageQueue.push("scroll", () => null);
}
collect() {
return mapFrameTree(this.mm, ScrollPosition.collect);
}
}
/**
* Listens for changes to input elements. Whenever the value of an input
* element changes we will re-collect data for the current frame tree and send
* a message to the parent process.
*
* Causes a SessionStore:update message to be sent that contains the form data
* for all reachable frames.
*
* Example:
* {
* formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}},
* children: [
* null,
* {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}}
* ]
* }
*/
class FormDataListener extends Handler {
constructor(store) {
super(store);
ssu.addDynamicFrameFilteredListener(this.mm, "input", this, true);
this.stateChangeNotifier.addObserver(this);
}
handleEvent() {
this.messageQueue.push("formdata", () => this.collect());
}
onPageLoadStarted() {
this.messageQueue.push("formdata", () => null);
}
collect() {
return mapFrameTree(this.mm, FormData.collect);
}
}
/**
* Listens for changes to docShell capabilities. Whenever a new load is started
* we need to re-check the list of capabilities and send message when it has
* changed.
*
* Causes a SessionStore:update message to be sent that contains the currently
* disabled docShell capabilities (all nsIDocShell.allow* properties set to
* false) as a string - i.e. capability names separate by commas.
*/
class DocShellCapabilitiesListener extends Handler {
constructor(store) {
super(store);
/**
* This field is used to compare the last docShell capabilities to the ones
* that have just been collected. If nothing changed we won't send a message.
*/
this._latestCapabilities = "";
this.stateChangeNotifier.addObserver(this);
}
onPageLoadStarted() {
// The order of docShell capabilities cannot change while we're running
// so calling join() without sorting before is totally sufficient.
let caps = DocShellCapabilities.collect(this.mm.docShell).join(",");
// Send new data only when the capability list changes.
if (caps != this._latestCapabilities) {
this._latestCapabilities = caps;
this.messageQueue.push("disallow", () => caps || null);
}
}
}
/**
* Listens for changes to the DOMSessionStorage. Whenever new keys are added,
* existing ones removed or changed, or the storage is cleared we will send a
* message to the parent process containing up-to-date sessionStorage data.
*
* Causes a SessionStore:update message to be sent that contains the current
* DOMSessionStorage contents. The data is a nested object using host names
* as keys and per-host DOMSessionStorage data as values.
*/
class SessionStorageListener extends Handler {
constructor(store) {
super(store);
// We don't want to send all the session storage data for all the frames
// for every change. So if only a few value changed we send them over as
// a "storagechange" event. If however for some reason before we send these
// changes we have to send over the entire sessions storage data, we just
// reset these changes.
this._changes = undefined;
// The event listener waiting for MozSessionStorageChanged events.
this._listener = null;
Services.obs.addObserver(this, "browser:purge-domain-data");
this.stateChangeNotifier.addObserver(this);
this.resetEventListener();
}
uninit() {
Services.obs.removeObserver(this, "browser:purge-domain-data");
}
observe() {
// Collect data on the next tick so that any other observer
// that needs to purge data can do its work first.
setTimeoutWithTarget(() => this.collect(), 0, this.mm.tabEventTarget);
}
resetChanges() {
this._changes = undefined;
}
resetEventListener() {
if (!this._listener) {
this._listener =
ssu.addDynamicFrameFilteredListener(this.mm, "MozSessionStorageChanged",
this, true);
}
}
removeEventListener() {
ssu.removeDynamicFrameFilteredListener(this.mm, "MozSessionStorageChanged",
this._listener, true);
this._listener = null;
}
handleEvent(event) {
if (!this.mm.docShell) {
return;
}
let {content} = this.mm;
// How much data does DOMSessionStorage contain?
let usage = content.windowUtils.getStorageUsage(event.storageArea);
// Don't store any data if we exceed the limit. Wipe any data we previously
// collected so that we don't confuse websites with partial state.
if (usage > Services.prefs.getIntPref(DOM_STORAGE_LIMIT_PREF)) {
this.messageQueue.push("storage", () => null);
this.removeEventListener();
this.resetChanges();
return;
}
let {url, key, newValue} = event;
let uri = Services.io.newURI(url);
let domain = uri.prePath;
if (!this._changes) {
this._changes = {};
}
if (!this._changes[domain]) {
this._changes[domain] = {};
}
this._changes[domain][key] = newValue;
this.messageQueue.push("storagechange", () => {
let tmp = this._changes;
// If there were multiple changes we send them merged.
// First one will collect all the changes the rest of
// these messages will be ignored.
this.resetChanges();
return tmp;
});
}
collect() {
if (!this.mm.docShell) {
return;
}
let {content} = this.mm;
// We need the entire session storage, let's reset the pending individual change
// messages.
this.resetChanges();
this.messageQueue.push("storage", () => SessionStorage.collect(content));
}
onPageLoadCompleted() {
this.collect();
}
onPageLoadStarted() {
this.resetEventListener();
this.collect();
}
}
/**
* Listen for changes to the privacy status of the tab.
* By definition, tabs start in non-private mode.
*
* Causes a SessionStore:update message to be sent for
* field "isPrivate". This message contains
* |true| if the tab is now private
* |null| if the tab is now public - the field is therefore
* not saved.
*/
class PrivacyListener extends Handler {
constructor(store) {
super(store);
this.mm.docShell.addWeakPrivacyTransitionObserver(this);
// Check that value at startup as it might have
// been set before the frame script was loaded.
if (this.mm.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) {
this.messageQueue.push("isPrivate", () => true);
}
}
// Ci.nsIPrivacyTransitionObserver
privateModeChanged(enabled) {
this.messageQueue.push("isPrivate", () => enabled || null);
}
}
PrivacyListener.prototype.QueryInterface =
ChromeUtils.generateQI([Ci.nsIPrivacyTransitionObserver,
Ci.nsISupportsWeakReference]);
/**
* A message queue that takes collected data and will take care of sending it
* to the chrome process. It allows flushing using synchronous messages and
* 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;
}
return val;
}
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:
debug("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:becomeActiveProcess",
];
class ContentSessionStore {
constructor(mm) {
this.mm = mm;
this.messageQueue = new MessageQueue(this);
this.stateChangeNotifier = new StateChangeNotifier(this);
this.epoch = 0;
this.contentRestoreInitialized = false;
XPCOMUtils.defineLazyGetter(this, "contentRestore",
() => {
this.contentRestoreInitialized = true;
return new ContentRestore(mm);
});
this.handlers = [
new EventListener(this),
new FormDataListener(this),
new SessionHistoryListener(this),
new SessionStorageListener(this),
new ScrollPositionListener(this),
new DocShellCapabilitiesListener(this),
new PrivacyListener(this),
this.stateChangeNotifier,
this.messageQueue,
];
MESSAGES.forEach(m => mm.addMessageListener(m, this));
// If we're browsing from the tab crashed UI to a blacklisted URI that keeps
// this browser non-remote, we'll handle that in a pagehide event.
mm.addEventListener("pagehide", 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.epoch && data.epoch != this.epoch) {
this.epoch = data.epoch;
}
switch (name) {
case "SessionStore:restoreHistory":
this.restoreHistory(data);
break;
case "SessionStore:restoreTabContent":
if (data.isRemotenessUpdate) {
let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS");
histogram.add("SessionStore:restoreTabContent",
Services.telemetry.msSystemNow() - data.requestTime);
}
this.restoreTabContent(data);
break;
case "SessionStore:resetRestore":
this.contentRestore.resetRestore();
break;
case "SessionStore:flush":
this.flush(data);
break;
case "SessionStore:becomeActiveProcess":
SessionHistoryListener.collect();
break;
default:
debug("received unknown message '" + name + "'");
break;
}
}
restoreHistory({epoch, tabData, loadArguments, isRemotenessUpdate}) {
this.contentRestore.restoreHistory(tabData, loadArguments, {
// Note: The callbacks passed here will only be used when a load starts
// that was not initiated by sessionstore itself. This can happen when
// some code calls browser.loadURI() or browser.reload() on a pending
// browser/tab.
onLoadStarted: () => {
// Notify the parent that the tab is no longer pending.
this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch});
},
onLoadFinished: () => {
// Tell SessionStore.jsm that it may want to restore some more tabs,
// since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch});
}
});
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
// For non-remote tabs, when restoreHistory finishes, we send a synchronous
// message to SessionStore.jsm 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.jsm.
let didStartLoad = this.contentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => {
// Tell SessionStore.jsm that it may want to restore some more tabs,
// since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time.
this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, 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 == "pagehide") {
this.handleRevivedTab();
} else 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});
// If we're browsing from the tab crashed UI to a URI that causes the tab
// to go remote again, we catch this in the unload event handler, because
// swapping out the non-remote browser for a remote one in
// tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide
// event to be fired.
this.handleRevivedTab();
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.
}
handleRevivedTab() {
let {content} = this.mm;
if (!content) {
this.mm.removeEventListener("pagehide", this);
return;
}
if (content.document.documentURI.startsWith("about:tabcrashed")) {
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
// Sanity check - we'd better be loading this in a non-remote browser.
throw new Error("We seem to be navigating away from about:tabcrashed in " +
"a non-remote browser. This should really never happen.");
}
this.mm.removeEventListener("pagehide", this);
// Notify the parent.
this.mm.sendAsyncMessage("SessionStore:crashedTabRevived");
}
}
}