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/. */
6 * Session Storage and Restoration
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.
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.
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.
26 * This service will always resume the session if the integer pref
27 * browser.startup.page is set to 3.
30 /* :::::::: Constants and Helpers ::::::::::::::: */
33 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
35 ChromeUtils.defineESModuleGetters(lazy, {
36 CrashMonitor: "resource://gre/modules/CrashMonitor.sys.mjs",
37 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
38 SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
40 "resource:///modules/sessionstore/StartupPerformance.sys.mjs",
43 const STATE_RUNNING_STR = "running";
45 const TYPE_NO_SESSION = 0;
46 const TYPE_RECOVER_SESSION = 1;
47 const TYPE_RESUME_SESSION = 2;
48 const TYPE_DEFER_SESSION = 3;
50 // 'browser.startup.page' preference value to resume the previous session.
51 const BROWSER_STARTUP_RESUME_SESSION = 3;
53 function warning(msg, exception) {
54 let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
63 Ci.nsIScriptError.warningFlag,
64 "component javascript"
66 Services.console.logMessage(consoleMsg);
69 var gOnceInitializedDeferred = (function () {
72 deferred.promise = new Promise((resolve, reject) => {
73 deferred.resolve = resolve;
74 deferred.reject = reject;
80 /* :::::::: The Service ::::::::::::::: */
82 export var SessionStartup = {
83 NO_SESSION: TYPE_NO_SESSION,
84 RECOVER_SESSION: TYPE_RECOVER_SESSION,
85 RESUME_SESSION: TYPE_RESUME_SESSION,
86 DEFER_SESSION: TYPE_DEFER_SESSION,
88 // The state to restore at startup.
93 // Stores whether the previous session crashed.
94 _previousSessionCrashed: null,
96 _resumeSessionEnabled: null,
98 /* ........ Global Event Handlers .............. */
101 * Initialize the component
104 Services.obs.notifyObservers(null, "sessionstore-init-started");
106 if (!AppConstants.DEBUG) {
107 lazy.StartupPerformance.init();
110 // do not need to initialize anything in auto-started private browsing sessions
111 if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
112 this._initialized = true;
113 gOnceInitializedDeferred.resolve();
118 Services.prefs.getBoolPref(
119 "browser.sessionstore.resuming_after_os_restart"
122 if (!Services.appinfo.restartedByOS) {
123 // We had set resume_session_once in order to resume after an OS restart,
124 // but we aren't automatically started by the OS (or else appinfo.restartedByOS
125 // would have been set). Therefore we should clear resume_session_once
126 // to avoid forcing a resume for a normal startup.
127 Services.prefs.setBoolPref(
128 "browser.sessionstore.resume_session_once",
132 Services.prefs.setBoolPref(
133 "browser.sessionstore.resuming_after_os_restart",
138 lazy.SessionFile.read().then(
139 this._onSessionFileRead.bind(this),
144 // Wrap a string as a nsISupports.
145 _createSupportsString(data) {
146 let string = Cc["@mozilla.org/supports-string;1"].createInstance(
154 * Complete initialization once the Session File has been read.
156 * @param source The Session State string read from disk.
157 * @param parsed The object obtained by parsing |source| as JSON.
159 _onSessionFileRead({ source, parsed, noFilesFound }) {
160 this._initialized = true;
162 // Let observers modify the state before it is used
163 let supportsStateString = this._createSupportsString(source);
164 Services.obs.notifyObservers(
166 "sessionstore-state-read"
168 let stateString = supportsStateString.data;
170 if (stateString != source) {
171 // The session has been modified by an add-on, reparse.
173 this._initialState = JSON.parse(stateString);
175 // That's not very good, an add-on has rewritten the initial
176 // state to something that won't parse.
177 warning("Observer rewrote the state to something that won't parse", ex);
180 // No need to reparse
181 this._initialState = parsed;
184 if (this._initialState == null) {
185 // No valid session found.
186 this._sessionType = this.NO_SESSION;
187 Services.obs.notifyObservers(null, "sessionstore-state-finalized");
188 gOnceInitializedDeferred.resolve();
192 let initialState = this._initialState;
193 Services.tm.idleDispatchToMainThread(() => {
194 let pinnedTabCount = initialState.windows.reduce((winAcc, win) => {
197 win.tabs.reduce((tabAcc, tab) => {
198 return tabAcc + (tab.pinned ? 1 : 0);
202 Services.telemetry.scalarSetMaximum(
203 "browser.engagement.max_concurrent_tab_pinned_count",
208 // If this is a normal restore then throw away any previous session.
209 if (!this.isAutomaticRestoreEnabled() && this._initialState) {
210 delete this._initialState.lastSessionState;
213 lazy.CrashMonitor.previousCheckpoints.then(checkpoints => {
215 // If the previous session finished writing the final state, we'll
216 // assume there was no crash.
217 this._previousSessionCrashed =
218 !checkpoints["sessionstore-final-state-write-complete"];
219 } else if (noFilesFound) {
220 // If the Crash Monitor could not load a checkpoints file it will
221 // provide null. This could occur on the first run after updating to
222 // a version including the Crash Monitor, or if the checkpoints file
223 // was removed, or on first startup with this profile, or after Firefox Reset.
225 // There was no checkpoints file and no sessionstore.js or its backups,
226 // so we will assume that this was a fresh profile.
227 this._previousSessionCrashed = false;
229 // If this is the first run after an update, sessionstore.js should
230 // still contain the session.state flag to indicate if the session
231 // crashed. If it is not present, we will assume this was not the first
232 // run after update and the checkpoints file was somehow corrupted or
233 // removed by a crash.
235 // If the session.state flag is present, we will fallback to using it
236 // for crash detection - If the last write of sessionstore.js had it
237 // set to "running", we crashed.
238 let stateFlagPresent =
239 this._initialState.session && this._initialState.session.state;
241 this._previousSessionCrashed =
243 this._initialState.session.state == STATE_RUNNING_STR;
246 // Report shutdown success via telemetry. Shortcoming here are
247 // being-killed-by-OS-shutdown-logic, shutdown freezing after
248 // session restore was written, etc.
250 .getHistogramById("SHUTDOWN_OK")
251 .add(!this._previousSessionCrashed);
253 Services.obs.addObserver(this, "sessionstore-windows-restored", true);
255 if (this.sessionType == this.NO_SESSION) {
256 this._initialState = null; // Reset the state.
258 Services.obs.addObserver(this, "browser:purge-session-history", true);
261 // We're ready. Notify everyone else.
262 Services.obs.notifyObservers(null, "sessionstore-state-finalized");
264 gOnceInitializedDeferred.resolve();
269 * Handle notifications
271 observe(subject, topic, data) {
273 case "sessionstore-windows-restored":
274 Services.obs.removeObserver(this, "sessionstore-windows-restored");
275 // Free _initialState after nsSessionStore is done with it.
276 this._initialState = null;
277 this._didRestore = true;
279 case "browser:purge-session-history":
280 Services.obs.removeObserver(this, "browser:purge-session-history");
281 // Reset all state on sanitization.
282 this._sessionType = this.NO_SESSION;
287 /* ........ Public API ................*/
289 get onceInitialized() {
290 return gOnceInitializedDeferred.promise;
294 * Get the session state as a jsval
297 return this._initialState;
301 * Determines whether automatic session restoration is enabled for this
302 * launch of the browser. This does not include crash restoration. In
303 * particular, if session restore is configured to restore only in case of
304 * crash, this method returns false.
307 isAutomaticRestoreEnabled() {
308 if (this._resumeSessionEnabled === null) {
309 this._resumeSessionEnabled =
310 !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing &&
311 (Services.prefs.getBoolPref(
312 "browser.sessionstore.resume_session_once"
314 Services.prefs.getIntPref("browser.startup.page") ==
315 BROWSER_STARTUP_RESUME_SESSION);
318 return this._resumeSessionEnabled;
322 * Determines whether there is a pending session restore.
327 this.sessionType == this.RECOVER_SESSION ||
328 this.sessionType == this.RESUME_SESSION
333 * Determines whether there is a pending session restore and if that will refer
337 willRestoreAsCrashed() {
338 return this.sessionType == this.RECOVER_SESSION;
342 * Returns a boolean or a promise that resolves to a boolean, indicating
343 * whether we will restore a session that ends up replacing the homepage.
344 * True guarantees that we'll restore a session; false means that we
345 * /probably/ won't do so.
346 * The browser uses this to avoid unnecessarily loading the homepage when
347 * restoring a session.
349 get willOverrideHomepage() {
350 // If the session file hasn't been read yet and resuming the session isn't
351 // enabled via prefs, go ahead and load the homepage. We may still replace
352 // it when recovering from a crash, which we'll only know after reading the
353 // session file, but waiting for that would delay loading the homepage in
354 // the non-crash case.
355 if (!this._initialState && !this.isAutomaticRestoreEnabled()) {
358 // If we've already restored the session, we won't override again.
359 if (this._didRestore) {
363 return new Promise(resolve => {
364 this.onceInitialized.then(() => {
365 // If there are valid windows with not only pinned tabs, signal that we
366 // will override the default homepage by restoring a session.
368 this.willRestore() &&
369 this._initialState &&
370 this._initialState.windows &&
371 (!this.willRestoreAsCrashed()
372 ? this._initialState.windows.filter(w => !w._maybeDontRestoreTabs)
373 : this._initialState.windows
374 ).some(w => w.tabs.some(t => !t.pinned))
381 * Get the type of pending session store, if any.
384 if (this._sessionType === null) {
385 let resumeFromCrash = Services.prefs.getBoolPref(
386 "browser.sessionstore.resume_from_crash"
388 // Set the startup type.
389 if (this.isAutomaticRestoreEnabled()) {
390 this._sessionType = this.RESUME_SESSION;
391 } else if (this._previousSessionCrashed && resumeFromCrash) {
392 this._sessionType = this.RECOVER_SESSION;
393 } else if (this._initialState) {
394 this._sessionType = this.DEFER_SESSION;
396 this._sessionType = this.NO_SESSION;
400 return this._sessionType;
404 * Get whether the previous session crashed.
406 get previousSessionCrashed() {
407 return this._previousSessionCrashed;
411 this._resumeSessionEnabled = null;
412 this._sessionType = null;
415 QueryInterface: ChromeUtils.generateQI([
417 "nsISupportsWeakReference",