tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SessionStartup.sys.mjs (15350B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * Session Storage and Restoration
      7 *
      8 * Overview
      9 * This service reads user's session file at startup, and makes a determination
     10 * as to whether the session should be restored. It will restore the session
     11 * under the circumstances described below.  If the auto-start Private Browsing
     12 * mode is active, however, the session is never restored.
     13 *
     14 * Crash Detection
     15 * The CrashMonitor is used to check if the final session state was successfully
     16 * written at shutdown of the last session. If we did not reach
     17 * 'sessionstore-final-state-write-complete', then it's assumed that the browser
     18 * has previously crashed and we should restore the session.
     19 *
     20 * Forced Restarts
     21 * In the event that a restart is required due to application update or extension
     22 * installation, set the browser.sessionstore.resume_session_once pref to true,
     23 * and the session will be restored the next time the browser starts.
     24 *
     25 * Always Resume
     26 * This service will always resume the session if the integer pref
     27 * browser.startup.page is set to 3.
     28 */
     29 
     30 /* :::::::: Constants and Helpers ::::::::::::::: */
     31 
     32 const lazy = {};
     33 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     34 
     35 ChromeUtils.defineESModuleGetters(lazy, {
     36  BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
     37  CrashMonitor: "resource://gre/modules/CrashMonitor.sys.mjs",
     38  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     39  SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
     40  StartupPerformance:
     41    "resource:///modules/sessionstore/StartupPerformance.sys.mjs",
     42  sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
     43 });
     44 
     45 const STATE_RUNNING_STR = "running";
     46 
     47 const TYPE_NO_SESSION = 0;
     48 const TYPE_RECOVER_SESSION = 1;
     49 const TYPE_RESUME_SESSION = 2;
     50 const TYPE_DEFER_SESSION = 3;
     51 
     52 // 'browser.startup.page' preference value to resume the previous session.
     53 const BROWSER_STARTUP_RESUME_SESSION = 3;
     54 
     55 var gOnceInitializedDeferred = Promise.withResolvers();
     56 
     57 /* :::::::: The Service ::::::::::::::: */
     58 
     59 export var SessionStartup = {
     60  NO_SESSION: TYPE_NO_SESSION,
     61  RECOVER_SESSION: TYPE_RECOVER_SESSION,
     62  RESUME_SESSION: TYPE_RESUME_SESSION,
     63  DEFER_SESSION: TYPE_DEFER_SESSION,
     64 
     65  // The state to restore at startup.
     66  _initialState: null,
     67  _sessionType: null,
     68  _initialized: false,
     69 
     70  // Stores whether the previous session crashed.
     71  _previousSessionCrashed: null,
     72 
     73  _resumeSessionEnabled: null,
     74 
     75  /* ........ Global Event Handlers .............. */
     76 
     77  /**
     78   * Initialize the component
     79   */
     80  init() {
     81    Services.obs.notifyObservers(null, "sessionstore-init-started");
     82 
     83    if (!AppConstants.DEBUG) {
     84      lazy.StartupPerformance.init();
     85    }
     86 
     87    // do not need to initialize anything in auto-started private browsing sessions
     88    if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
     89      this._initialized = true;
     90      gOnceInitializedDeferred.resolve();
     91      return;
     92    }
     93 
     94    if (
     95      Services.prefs.getBoolPref(
     96        "browser.sessionstore.resuming_after_os_restart"
     97      )
     98    ) {
     99      lazy.sessionStoreLogger.debug("resuming_after_os_restart");
    100      if (!Services.appinfo.restartedByOS) {
    101        // We had set resume_session_once in order to resume after an OS restart,
    102        // but we aren't automatically started by the OS (or else appinfo.restartedByOS
    103        // would have been set). Therefore we should clear resume_session_once
    104        // to avoid forcing a resume for a normal startup.
    105        Services.prefs.setBoolPref(
    106          "browser.sessionstore.resume_session_once",
    107          false
    108        );
    109      }
    110      Services.prefs.setBoolPref(
    111        "browser.sessionstore.resuming_after_os_restart",
    112        false
    113      );
    114    }
    115 
    116    lazy.SessionFile.read().then(
    117      result => {
    118        lazy.sessionStoreLogger.debug(
    119          `Completed SessionFile.read() with result.origin: ${result.origin}`
    120        );
    121        return this._onSessionFileRead(result);
    122      },
    123      err => {
    124        // SessionFile.read catches most expected failures,
    125        // so a promise rejection here should be logged as an error
    126        lazy.sessionStoreLogger.error("Failure from _onSessionFileRead", err);
    127      }
    128    );
    129  },
    130 
    131  // Wrap a string as a nsISupports.
    132  _createSupportsString(data) {
    133    let string = Cc["@mozilla.org/supports-string;1"].createInstance(
    134      Ci.nsISupportsString
    135    );
    136    string.data = data;
    137    return string;
    138  },
    139 
    140  /**
    141   * Complete initialization once the Session File has been read.
    142   *
    143   * @param source The Session State string read from disk.
    144   * @param parsed The object obtained by parsing |source| as JSON.
    145   */
    146  _onSessionFileRead({ source, parsed, noFilesFound }) {
    147    this._initialized = true;
    148    const crashReasons = {
    149      FINAL_STATE_WRITING_INCOMPLETE: "final-state-write-incomplete",
    150      SESSION_STATE_FLAG_MISSING:
    151        "session-state-missing-or-running-at-last-write",
    152    };
    153 
    154    // Let observers modify the state before it is used
    155    let supportsStateString = this._createSupportsString(source);
    156    Services.obs.notifyObservers(
    157      supportsStateString,
    158      "sessionstore-state-read"
    159    );
    160    let stateString = supportsStateString.data;
    161 
    162    if (stateString != source) {
    163      // The session has been modified by an add-on, reparse.
    164      lazy.sessionStoreLogger.debug(
    165        "After sessionstore-state-read, session has been modified"
    166      );
    167      try {
    168        this._initialState = JSON.parse(stateString);
    169      } catch (ex) {
    170        // That's not very good, an add-on has rewritten the initial
    171        // state to something that won't parse.
    172        lazy.sessionStoreLogger.error(
    173          "'sessionstore-state-read' observer rewrote the state to something that won't parse",
    174          ex
    175        );
    176      }
    177    } else {
    178      // No need to reparse
    179      this._initialState = parsed;
    180    }
    181 
    182    if (this._initialState == null) {
    183      // No valid session found.
    184      this._sessionType = this.NO_SESSION;
    185      lazy.sessionStoreLogger.debug("No valid session found");
    186      Services.obs.notifyObservers(null, "sessionstore-state-finalized");
    187      gOnceInitializedDeferred.resolve();
    188      return;
    189    }
    190 
    191    let initialState = this._initialState;
    192    Services.tm.idleDispatchToMainThread(() => {
    193      let pinnedTabCount = initialState.windows.reduce((winAcc, win) => {
    194        return (
    195          winAcc +
    196          win.tabs.reduce((tabAcc, tab) => {
    197            return tabAcc + (tab.pinned ? 1 : 0);
    198          }, 0)
    199        );
    200      }, 0);
    201      lazy.sessionStoreLogger.debug(
    202        `initialState contains ${pinnedTabCount} pinned tabs`
    203      );
    204 
    205      lazy.BrowserUsageTelemetry.updateMaxTabPinnedCount(pinnedTabCount);
    206    }, 60000);
    207 
    208    let isAutomaticRestoreEnabled = this.isAutomaticRestoreEnabled();
    209    lazy.sessionStoreLogger.debug(
    210      `isAutomaticRestoreEnabled: ${isAutomaticRestoreEnabled}`
    211    );
    212    // If this is a normal restore then throw away any previous session.
    213    if (!isAutomaticRestoreEnabled && this._initialState) {
    214      lazy.sessionStoreLogger.debug(
    215        "Discarding previous session as we have initialState"
    216      );
    217      delete this._initialState.lastSessionState;
    218    }
    219 
    220    let previousSessionCrashedReason = "N/A";
    221    lazy.CrashMonitor.previousCheckpoints.then(checkpoints => {
    222      if (checkpoints) {
    223        // If the previous session finished writing the final state, we'll
    224        // assume there was no crash.
    225        this._previousSessionCrashed =
    226          !checkpoints["sessionstore-final-state-write-complete"];
    227        if (!checkpoints["sessionstore-final-state-write-complete"]) {
    228          previousSessionCrashedReason =
    229            crashReasons.FINAL_STATE_WRITING_INCOMPLETE;
    230        }
    231      } else if (noFilesFound) {
    232        // If the Crash Monitor could not load a checkpoints file it will
    233        // provide null. This could occur on the first run after updating to
    234        // a version including the Crash Monitor, or if the checkpoints file
    235        // was removed, or on first startup with this profile, or after Firefox Reset.
    236 
    237        // There was no checkpoints file and no sessionstore.js or its backups,
    238        // so we will assume that this was a fresh profile.
    239        this._previousSessionCrashed = false;
    240      } else {
    241        // If this is the first run after an update, sessionstore.js should
    242        // still contain the session.state flag to indicate if the session
    243        // crashed. If it is not present, we will assume this was not the first
    244        // run after update and the checkpoints file was somehow corrupted or
    245        // removed by a crash.
    246        //
    247        // If the session.state flag is present, we will fallback to using it
    248        // for crash detection - If the last write of sessionstore.js had it
    249        // set to "running", we crashed.
    250        let stateFlagPresent =
    251          this._initialState.session && this._initialState.session.state;
    252 
    253        this._previousSessionCrashed =
    254          !stateFlagPresent ||
    255          this._initialState.session.state == STATE_RUNNING_STR;
    256        if (
    257          !stateFlagPresent ||
    258          this._initialState.session.state == STATE_RUNNING_STR
    259        ) {
    260          previousSessionCrashedReason =
    261            crashReasons.SESSION_STATE_FLAG_MISSING;
    262        }
    263      }
    264 
    265      // Report shutdown success via telemetry. Shortcoming here are
    266      // being-killed-by-OS-shutdown-logic, shutdown freezing after
    267      // session restore was written, etc.
    268      Glean.sessionRestore.shutdownOk[
    269        this._previousSessionCrashed ? "false" : "true"
    270      ].add();
    271      Glean.sessionRestore.shutdownSuccessSessionStartup.record({
    272        shutdown_ok: this._previousSessionCrashed.toString(),
    273        shutdown_reason: previousSessionCrashedReason,
    274      });
    275      lazy.sessionStoreLogger.debug(
    276        `Previous shutdown ok? ${this._previousSessionCrashed}, reason: ${previousSessionCrashedReason}`
    277      );
    278 
    279      Services.obs.addObserver(this, "sessionstore-windows-restored", true);
    280 
    281      if (this.sessionType == this.NO_SESSION) {
    282        lazy.sessionStoreLogger.debug("Will restore no session");
    283        this._initialState = null; // Reset the state.
    284      } else {
    285        Services.obs.addObserver(this, "browser:purge-session-history", true);
    286      }
    287 
    288      // We're ready. Notify everyone else.
    289      Services.obs.notifyObservers(null, "sessionstore-state-finalized");
    290 
    291      gOnceInitializedDeferred.resolve();
    292    });
    293  },
    294 
    295  /**
    296   * Handle notifications
    297   */
    298  observe(subject, topic) {
    299    switch (topic) {
    300      case "sessionstore-windows-restored":
    301        Services.obs.removeObserver(this, "sessionstore-windows-restored");
    302        lazy.sessionStoreLogger.debug(`sessionstore-windows-restored`);
    303        // Free _initialState after nsSessionStore is done with it.
    304        this._initialState = null;
    305        this._didRestore = true;
    306        break;
    307      case "browser:purge-session-history":
    308        Services.obs.removeObserver(this, "browser:purge-session-history");
    309        // Reset all state on sanitization.
    310        this._sessionType = this.NO_SESSION;
    311        break;
    312    }
    313  },
    314 
    315  /* ........ Public API ................*/
    316 
    317  get onceInitialized() {
    318    return gOnceInitializedDeferred.promise;
    319  },
    320 
    321  /**
    322   * Get the session state as a jsval
    323   */
    324  get state() {
    325    return this._initialState;
    326  },
    327 
    328  /**
    329   * Determines whether automatic session restoration is enabled for this
    330   * launch of the browser. This does not include crash restoration. In
    331   * particular, if session restore is configured to restore only in case of
    332   * crash, this method returns false.
    333   *
    334   * @returns bool
    335   */
    336  isAutomaticRestoreEnabled() {
    337    if (this._resumeSessionEnabled === null) {
    338      this._resumeSessionEnabled =
    339        !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing &&
    340        (Services.prefs.getBoolPref(
    341          "browser.sessionstore.resume_session_once"
    342        ) ||
    343          Services.prefs.getIntPref("browser.startup.page") ==
    344            BROWSER_STARTUP_RESUME_SESSION);
    345    }
    346 
    347    return this._resumeSessionEnabled;
    348  },
    349 
    350  /**
    351   * Determines whether there is a pending session restore.
    352   *
    353   * @returns bool
    354   */
    355  willRestore() {
    356    return (
    357      this.sessionType == this.RECOVER_SESSION ||
    358      this.sessionType == this.RESUME_SESSION
    359    );
    360  },
    361 
    362  /**
    363   * Determines whether there is a pending session restore and if that will refer
    364   * back to a crash.
    365   *
    366   * @returns bool
    367   */
    368  willRestoreAsCrashed() {
    369    return this.sessionType == this.RECOVER_SESSION;
    370  },
    371 
    372  /**
    373   * Returns a boolean or a promise that resolves to a boolean, indicating
    374   * whether we will restore a session that ends up replacing the homepage.
    375   * True guarantees that we'll restore a session; false means that we
    376   * /probably/ won't do so.
    377   * The browser uses this to avoid unnecessarily loading the homepage when
    378   * restoring a session.
    379   */
    380  get willOverrideHomepage() {
    381    // If the session file hasn't been read yet and resuming the session isn't
    382    // enabled via prefs, go ahead and load the homepage. We may still replace
    383    // it when recovering from a crash, which we'll only know after reading the
    384    // session file, but waiting for that would delay loading the homepage in
    385    // the non-crash case.
    386    if (!this._initialState && !this.isAutomaticRestoreEnabled()) {
    387      return false;
    388    }
    389    // If we've already restored the session, we won't override again.
    390    if (this._didRestore) {
    391      return false;
    392    }
    393 
    394    return new Promise(resolve => {
    395      this.onceInitialized.then(() => {
    396        // If there are valid windows with not only pinned tabs, signal that we
    397        // will override the default homepage by restoring a session.
    398        resolve(
    399          this.willRestore() &&
    400            this._initialState &&
    401            this._initialState.windows &&
    402            (!this.willRestoreAsCrashed()
    403              ? this._initialState.windows.filter(w => !w._maybeDontRestoreTabs)
    404              : this._initialState.windows
    405            ).some(w => w.tabs.some(t => !t.pinned))
    406        );
    407      });
    408    });
    409  },
    410 
    411  /**
    412   * Get the type of pending session store, if any.
    413   */
    414  get sessionType() {
    415    if (this._sessionType === null) {
    416      let resumeFromCrash = Services.prefs.getBoolPref(
    417        "browser.sessionstore.resume_from_crash"
    418      );
    419      // Set the startup type.
    420      if (this.isAutomaticRestoreEnabled()) {
    421        this._sessionType = this.RESUME_SESSION;
    422      } else if (this._previousSessionCrashed && resumeFromCrash) {
    423        this._sessionType = this.RECOVER_SESSION;
    424      } else if (this._initialState) {
    425        this._sessionType = this.DEFER_SESSION;
    426      } else {
    427        this._sessionType = this.NO_SESSION;
    428      }
    429    }
    430 
    431    return this._sessionType;
    432  },
    433 
    434  /**
    435   * Get whether the previous session crashed.
    436   */
    437  get previousSessionCrashed() {
    438    return this._previousSessionCrashed;
    439  },
    440 
    441  resetForTest() {
    442    this._resumeSessionEnabled = null;
    443    this._sessionType = null;
    444  },
    445 
    446  QueryInterface: ChromeUtils.generateQI([
    447    "nsIObserver",
    448    "nsISupportsWeakReference",
    449  ]),
    450 };