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