summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/SessionStartup.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/sessionstore/SessionStartup.sys.mjs420
1 files changed, 420 insertions, 0 deletions
diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs
new file mode 100644
index 0000000000..37d7bf387e
--- /dev/null
+++ b/browser/components/sessionstore/SessionStartup.sys.mjs
@@ -0,0 +1,420 @@
+/* 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 = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
+ StartupPerformance:
+ "resource:///modules/sessionstore/StartupPerformance.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "CrashMonitor",
+ "resource://gre/modules/CrashMonitor.jsm"
+);
+
+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");
+ 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;
+
+ // 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;
+ }
+
+ 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"
+ ];
+ } 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;
+ }
+
+ // 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.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, data) {
+ 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",
+ ]),
+};