/* 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/. */ /** * 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 lazy = {}; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; ChromeUtils.defineESModuleGetters(lazy, { CrashMonitor: "resource://gre/modules/CrashMonitor.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs", StartupPerformance: "resource:///modules/sessionstore/StartupPerformance.sys.mjs", }); const STATE_RUNNING_STR = "running"; const TYPE_NO_SESSION = 0; const TYPE_RECOVER_SESSION = 1; const TYPE_RESUME_SESSION = 2; const TYPE_DEFER_SESSION = 3; // 'browser.startup.page' preference value to resume the previous session. const BROWSER_STARTUP_RESUME_SESSION = 3; function warning(msg, exception) { let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance( Ci.nsIScriptError ); consoleMsg.init( msg, exception.fileName, null, exception.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 ::::::::::::::: */ export var SessionStartup = { NO_SESSION: TYPE_NO_SESSION, RECOVER_SESSION: TYPE_RECOVER_SESSION, RESUME_SESSION: TYPE_RESUME_SESSION, DEFER_SESSION: TYPE_DEFER_SESSION, // The state to restore at startup. _initialState: null, _sessionType: null, _initialized: false, // Stores whether the previous session crashed. _previousSessionCrashed: null, _resumeSessionEnabled: null, /* ........ Global Event Handlers .............. */ /** * Initialize the component */ init() { Services.obs.notifyObservers(null, "sessionstore-init-started"); if (!AppConstants.DEBUG) { lazy.StartupPerformance.init(); } // do not need to initialize anything in auto-started private browsing sessions if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { this._initialized = true; gOnceInitializedDeferred.resolve(); return; } if ( Services.prefs.getBoolPref( "browser.sessionstore.resuming_after_os_restart" ) ) { if (!Services.appinfo.restartedByOS) { // We had set resume_session_once in order to resume after an OS restart, // but we aren't automatically started by the OS (or else appinfo.restartedByOS // would have been set). Therefore we should clear resume_session_once // to avoid forcing a resume for a normal startup. Services.prefs.setBoolPref( "browser.sessionstore.resume_session_once", false ); } Services.prefs.setBoolPref( "browser.sessionstore.resuming_after_os_restart", false ); } lazy.SessionFile.read().then( this._onSessionFileRead.bind(this), console.error ); }, // Wrap a string as a nsISupports. _createSupportsString(data) { let string = Cc["@mozilla.org/supports-string;1"].createInstance( Ci.nsISupportsString ); string.data = data; 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({ source, parsed, noFilesFound }) { this._initialized = true; const crashReasons = { FINAL_STATE_WRITING_INCOMPLETE: "final-state-write-incomplete", SESSION_STATE_FLAG_MISSING: "session-state-missing-or-running-at-last-write", }; // 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 = this.NO_SESSION; Services.obs.notifyObservers(null, "sessionstore-state-finalized"); gOnceInitializedDeferred.resolve(); return; } let initialState = this._initialState; Services.tm.idleDispatchToMainThread(() => { let pinnedTabCount = initialState.windows.reduce((winAcc, win) => { return ( winAcc + win.tabs.reduce((tabAcc, tab) => { return tabAcc + (tab.pinned ? 1 : 0); }, 0) ); }, 0); Services.telemetry.scalarSetMaximum( "browser.engagement.max_concurrent_tab_pinned_count", pinnedTabCount ); }, 60000); // If this is a normal restore then throw away any previous session. if (!this.isAutomaticRestoreEnabled() && this._initialState) { delete this._initialState.lastSessionState; } let previousSessionCrashedReason = "N/A"; lazy.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"]; if (!checkpoints["sessionstore-final-state-write-complete"]) { previousSessionCrashedReason = crashReasons.FINAL_STATE_WRITING_INCOMPLETE; } } else if (noFilesFound) { // 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. // 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; if ( !stateFlagPresent || this._initialState.session.state == STATE_RUNNING_STR ) { previousSessionCrashedReason = crashReasons.SESSION_STATE_FLAG_MISSING; } } // 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); Services.telemetry.recordEvent( "session_restore", "shutdown_success", "session_startup", null, { shutdown_ok: this._previousSessionCrashed.toString(), shutdown_reason: previousSessionCrashedReason, } ); Services.obs.addObserver(this, "sessionstore-windows-restored", true); if (this.sessionType == this.NO_SESSION) { this._initialState = null; // Reset the state. } else { 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(subject, topic) { switch (topic) { case "sessionstore-windows-restored": Services.obs.removeObserver(this, "sessionstore-windows-restored"); // Free _initialState after nsSessionStore is done with it. this._initialState = null; this._didRestore = true; break; case "browser:purge-session-history": Services.obs.removeObserver(this, "browser:purge-session-history"); // Reset all state on sanitization. this._sessionType = this.NO_SESSION; break; } }, /* ........ Public API ................*/ get onceInitialized() { return gOnceInitializedDeferred.promise; }, /** * Get the session state as a jsval */ get state() { return this._initialState; }, /** * 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() { if (this._resumeSessionEnabled === null) { this._resumeSessionEnabled = !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing && (Services.prefs.getBoolPref( "browser.sessionstore.resume_session_once" ) || Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION); } return this._resumeSessionEnabled; }, /** * Determines whether there is a pending session restore. * @returns bool */ willRestore() { return ( this.sessionType == this.RECOVER_SESSION || this.sessionType == this.RESUME_SESSION ); }, /** * Determines whether there is a pending session restore and if that will refer * back to a crash. * @returns bool */ willRestoreAsCrashed() { return this.sessionType == this.RECOVER_SESSION; }, /** * Returns a boolean or a promise that resolves to a boolean, indicating * whether we will restore a session that ends up replacing the homepage. * True guarantees that we'll restore a session; false means that we * /probably/ won't do so. * The browser uses this to avoid unnecessarily loading the homepage when * restoring a session. */ get willOverrideHomepage() { // If the session file hasn't been read yet and resuming the session isn't // enabled via prefs, go ahead and load the homepage. We may still replace // it when recovering from a crash, which we'll only know after reading the // session file, but waiting for that would delay loading the homepage in // the non-crash case. if (!this._initialState && !this.isAutomaticRestoreEnabled()) { return false; } // If we've already restored the session, we won't override again. if (this._didRestore) { return false; } return new Promise(resolve => { this.onceInitialized.then(() => { // If there are valid windows with not only pinned tabs, signal that we // will override the default homepage by restoring a session. resolve( this.willRestore() && this._initialState && this._initialState.windows && (!this.willRestoreAsCrashed() ? this._initialState.windows.filter(w => !w._maybeDontRestoreTabs) : this._initialState.windows ).some(w => w.tabs.some(t => !t.pinned)) ); }); }); }, /** * Get the type of pending session store, if any. */ get sessionType() { if (this._sessionType === null) { let resumeFromCrash = Services.prefs.getBoolPref( "browser.sessionstore.resume_from_crash" ); // Set the startup type. if (this.isAutomaticRestoreEnabled()) { this._sessionType = this.RESUME_SESSION; } else if (this._previousSessionCrashed && resumeFromCrash) { this._sessionType = this.RECOVER_SESSION; } else if (this._initialState) { this._sessionType = this.DEFER_SESSION; } else { this._sessionType = this.NO_SESSION; } } return this._sessionType; }, /** * Get whether the previous session crashed. */ get previousSessionCrashed() { return this._previousSessionCrashed; }, resetForTest() { this._resumeSessionEnabled = null; this._sessionType = null; }, QueryInterface: ChromeUtils.generateQI([ "nsIObserver", "nsISupportsWeakReference", ]), };