diff options
Diffstat (limited to 'browser/components/sessionstore/nsSessionStartup.js')
-rw-r--r-- | browser/components/sessionstore/nsSessionStartup.js | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/browser/components/sessionstore/nsSessionStartup.js b/browser/components/sessionstore/nsSessionStartup.js new file mode 100644 index 000000000..7593c48ec --- /dev/null +++ b/browser/components/sessionstore/nsSessionStartup.js @@ -0,0 +1,353 @@ +/* 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"; + +/** + * Session Storage and Restoration + * + * Overview + * This service reads user's session file at startup, and makes a determination + * as to whether the session should be restored. It will restore the session + * under the circumstances described below. If the auto-start Private Browsing + * mode is active, however, the session is never restored. + * + * Crash Detection + * The CrashMonitor is used to check if the final session state was successfully + * written at shutdown of the last session. If we did not reach + * 'sessionstore-final-state-write-complete', then it's assumed that the browser + * has previously crashed and we should restore the session. + * + * Forced Restarts + * In the event that a restart is required due to application update or extension + * installation, set the browser.sessionstore.resume_session_once pref to true, + * and the session will be restored the next time the browser starts. + * + * Always Resume + * This service will always resume the session if the integer pref + * browser.startup.page is set to 3. + */ + +/* :::::::: Constants and Helpers ::::::::::::::: */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", + "resource:///modules/sessionstore/SessionFile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "StartupPerformance", + "resource:///modules/sessionstore/StartupPerformance.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor", + "resource://gre/modules/CrashMonitor.jsm"); + +const STATE_RUNNING_STR = "running"; + +// 'browser.startup.page' preference value to resume the previous session. +const BROWSER_STARTUP_RESUME_SESSION = 3; + +function debug(aMsg) { + aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n"); + Services.console.logStringMessage(aMsg); +} +function warning(aMsg, aException) { + let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError); +consoleMsg.init(aMsg, aException.fileName, null, aException.lineNumber, 0, Ci.nsIScriptError.warningFlag, "component javascript"); + Services.console.logMessage(consoleMsg); +} + +var gOnceInitializedDeferred = (function () { + let deferred = {}; + + deferred.promise = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + return deferred; +})(); + +/* :::::::: The Service ::::::::::::::: */ + +function SessionStartup() { +} + +SessionStartup.prototype = { + + // the state to restore at startup + _initialState: null, + _sessionType: Ci.nsISessionStartup.NO_SESSION, + _initialized: false, + + // Stores whether the previous session crashed. + _previousSessionCrashed: null, + +/* ........ Global Event Handlers .............. */ + + /** + * Initialize the component + */ + init: function sss_init() { + Services.obs.notifyObservers(null, "sessionstore-init-started", null); + StartupPerformance.init(); + + // do not need to initialize anything in auto-started private browsing sessions + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + this._initialized = true; + gOnceInitializedDeferred.resolve(); + return; + } + + SessionFile.read().then( + this._onSessionFileRead.bind(this), + console.error + ); + }, + + // Wrap a string as a nsISupports + _createSupportsString: function ssfi_createSupportsString(aData) { + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = aData; + return string; + }, + + /** + * Complete initialization once the Session File has been read + * + * @param source The Session State string read from disk. + * @param parsed The object obtained by parsing |source| as JSON. + */ + _onSessionFileRead: function ({source, parsed, noFilesFound}) { + this._initialized = true; + + // Let observers modify the state before it is used + let supportsStateString = this._createSupportsString(source); + Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", ""); + let stateString = supportsStateString.data; + + if (stateString != source) { + // The session has been modified by an add-on, reparse. + try { + this._initialState = JSON.parse(stateString); + } catch (ex) { + // That's not very good, an add-on has rewritten the initial + // state to something that won't parse. + warning("Observer rewrote the state to something that won't parse", ex); + } + } else { + // No need to reparse + this._initialState = parsed; + } + + if (this._initialState == null) { + // No valid session found. + this._sessionType = Ci.nsISessionStartup.NO_SESSION; + Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); + gOnceInitializedDeferred.resolve(); + return; + } + + let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); + let shouldResumeSession = shouldResumeSessionOnce || + Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; + + // If this is a normal restore then throw away any previous session + if (!shouldResumeSessionOnce && this._initialState) { + delete this._initialState.lastSessionState; + } + + let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"); + + CrashMonitor.previousCheckpoints.then(checkpoints => { + if (checkpoints) { + // If the previous session finished writing the final state, we'll + // assume there was no crash. + this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"]; + + } else { + // If the Crash Monitor could not load a checkpoints file it will + // provide null. This could occur on the first run after updating to + // a version including the Crash Monitor, or if the checkpoints file + // was removed, or on first startup with this profile, or after Firefox Reset. + + if (noFilesFound) { + // There was no checkpoints file and no sessionstore.js or its backups + // so we will assume that this was a fresh profile. + this._previousSessionCrashed = false; + + } else { + // If this is the first run after an update, sessionstore.js should + // still contain the session.state flag to indicate if the session + // crashed. If it is not present, we will assume this was not the first + // run after update and the checkpoints file was somehow corrupted or + // removed by a crash. + // + // If the session.state flag is present, we will fallback to using it + // for crash detection - If the last write of sessionstore.js had it + // set to "running", we crashed. + let stateFlagPresent = (this._initialState.session && + this._initialState.session.state); + + + this._previousSessionCrashed = !stateFlagPresent || + (this._initialState.session.state == STATE_RUNNING_STR); + } + } + + // Report shutdown success via telemetry. Shortcoming here are + // being-killed-by-OS-shutdown-logic, shutdown freezing after + // session restore was written, etc. + Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed); + + // set the startup type + if (this._previousSessionCrashed && resumeFromCrash) + this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION; + else if (!this._previousSessionCrashed && shouldResumeSession) + this._sessionType = Ci.nsISessionStartup.RESUME_SESSION; + else if (this._initialState) + this._sessionType = Ci.nsISessionStartup.DEFER_SESSION; + else + this._initialState = null; // reset the state + + Services.obs.addObserver(this, "sessionstore-windows-restored", true); + + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) + Services.obs.addObserver(this, "browser:purge-session-history", true); + + // We're ready. Notify everyone else. + Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); + gOnceInitializedDeferred.resolve(); + }); + }, + + /** + * Handle notifications + */ + observe: function sss_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "app-startup": + Services.obs.addObserver(this, "final-ui-startup", true); + Services.obs.addObserver(this, "quit-application", true); + break; + case "final-ui-startup": + Services.obs.removeObserver(this, "final-ui-startup"); + Services.obs.removeObserver(this, "quit-application"); + this.init(); + break; + case "quit-application": + // no reason for initializing at this point (cf. bug 409115) + Services.obs.removeObserver(this, "final-ui-startup"); + Services.obs.removeObserver(this, "quit-application"); + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) + Services.obs.removeObserver(this, "browser:purge-session-history"); + break; + case "sessionstore-windows-restored": + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + // free _initialState after nsSessionStore is done with it + this._initialState = null; + break; + case "browser:purge-session-history": + Services.obs.removeObserver(this, "browser:purge-session-history"); + // reset all state on sanitization + this._sessionType = Ci.nsISessionStartup.NO_SESSION; + break; + } + }, + +/* ........ Public API ................*/ + + get onceInitialized() { + return gOnceInitializedDeferred.promise; + }, + + /** + * Get the session state as a jsval + */ + get state() { + return this._initialState; + }, + + /** + * Determines whether there is a pending session restore. Should only be + * called after initialization has completed. + * @returns bool + */ + doRestore: function sss_doRestore() { + return this._willRestore(); + }, + + /** + * Determines whether automatic session restoration is enabled for this + * launch of the browser. This does not include crash restoration. In + * particular, if session restore is configured to restore only in case of + * crash, this method returns false. + * @returns bool + */ + isAutomaticRestoreEnabled: function () { + return Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") || + Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; + }, + + /** + * Determines whether there is a pending session restore. + * @returns bool + */ + _willRestore: function () { + return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || + this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; + }, + + /** + * Returns whether we will restore a session that ends up replacing the + * homepage. The browser uses this to not start loading the homepage if + * we're going to stop its load anyway shortly after. + * + * This is meant to be an optimization for the average case that loading the + * session file finishes before we may want to start loading the default + * homepage. Should this be called before the session file has been read it + * will just return false. + * + * @returns bool + */ + get willOverrideHomepage() { + if (this._initialState && this._willRestore()) { + let windows = this._initialState.windows || null; + // If there are valid windows with not only pinned tabs, signal that we + // will override the default homepage by restoring a session. + return windows && windows.some(w => w.tabs.some(t => !t.pinned)); + } + return false; + }, + + /** + * Get the type of pending session store, if any. + */ + get sessionType() { + return this._sessionType; + }, + + /** + * Get whether the previous session crashed. + */ + get previousSessionCrashed() { + return this._previousSessionCrashed; + }, + + /* ........ QueryInterface .............. */ + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference, + Ci.nsISessionStartup]), + classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}") +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]); |