Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / browser / components / sessionstore / SessionStartup.sys.mjs
blobff3ba55176293d62075caa34db4048668bcb5115
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/. */
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  */
30 /* :::::::: Constants and Helpers ::::::::::::::: */
32 const lazy = {};
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",
39   StartupPerformance:
40     "resource:///modules/sessionstore/StartupPerformance.sys.mjs",
41 });
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(
55     Ci.nsIScriptError
56   );
57   consoleMsg.init(
58     msg,
59     exception.fileName,
60     null,
61     exception.lineNumber,
62     0,
63     Ci.nsIScriptError.warningFlag,
64     "component javascript"
65   );
66   Services.console.logMessage(consoleMsg);
69 var gOnceInitializedDeferred = (function () {
70   let deferred = {};
72   deferred.promise = new Promise((resolve, reject) => {
73     deferred.resolve = resolve;
74     deferred.reject = reject;
75   });
77   return deferred;
78 })();
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.
89   _initialState: null,
90   _sessionType: null,
91   _initialized: false,
93   // Stores whether the previous session crashed.
94   _previousSessionCrashed: null,
96   _resumeSessionEnabled: null,
98   /* ........ Global Event Handlers .............. */
100   /**
101    * Initialize the component
102    */
103   init() {
104     Services.obs.notifyObservers(null, "sessionstore-init-started");
106     if (!AppConstants.DEBUG) {
107       lazy.StartupPerformance.init();
108     }
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();
114       return;
115     }
117     if (
118       Services.prefs.getBoolPref(
119         "browser.sessionstore.resuming_after_os_restart"
120       )
121     ) {
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",
129           false
130         );
131       }
132       Services.prefs.setBoolPref(
133         "browser.sessionstore.resuming_after_os_restart",
134         false
135       );
136     }
138     lazy.SessionFile.read().then(
139       this._onSessionFileRead.bind(this),
140       console.error
141     );
142   },
144   // Wrap a string as a nsISupports.
145   _createSupportsString(data) {
146     let string = Cc["@mozilla.org/supports-string;1"].createInstance(
147       Ci.nsISupportsString
148     );
149     string.data = data;
150     return string;
151   },
153   /**
154    * Complete initialization once the Session File has been read.
155    *
156    * @param source The Session State string read from disk.
157    * @param parsed The object obtained by parsing |source| as JSON.
158    */
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(
165       supportsStateString,
166       "sessionstore-state-read"
167     );
168     let stateString = supportsStateString.data;
170     if (stateString != source) {
171       // The session has been modified by an add-on, reparse.
172       try {
173         this._initialState = JSON.parse(stateString);
174       } catch (ex) {
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);
178       }
179     } else {
180       // No need to reparse
181       this._initialState = parsed;
182     }
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();
189       return;
190     }
192     let initialState = this._initialState;
193     Services.tm.idleDispatchToMainThread(() => {
194       let pinnedTabCount = initialState.windows.reduce((winAcc, win) => {
195         return (
196           winAcc +
197           win.tabs.reduce((tabAcc, tab) => {
198             return tabAcc + (tab.pinned ? 1 : 0);
199           }, 0)
200         );
201       }, 0);
202       Services.telemetry.scalarSetMaximum(
203         "browser.engagement.max_concurrent_tab_pinned_count",
204         pinnedTabCount
205       );
206     }, 60000);
208     // If this is a normal restore then throw away any previous session.
209     if (!this.isAutomaticRestoreEnabled() && this._initialState) {
210       delete this._initialState.lastSessionState;
211     }
213     lazy.CrashMonitor.previousCheckpoints.then(checkpoints => {
214       if (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;
228       } else {
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.
234         //
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 =
242           !stateFlagPresent ||
243           this._initialState.session.state == STATE_RUNNING_STR;
244       }
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.
249       Services.telemetry
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.
257       } else {
258         Services.obs.addObserver(this, "browser:purge-session-history", true);
259       }
261       // We're ready. Notify everyone else.
262       Services.obs.notifyObservers(null, "sessionstore-state-finalized");
264       gOnceInitializedDeferred.resolve();
265     });
266   },
268   /**
269    * Handle notifications
270    */
271   observe(subject, topic, data) {
272     switch (topic) {
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;
278         break;
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;
283         break;
284     }
285   },
287   /* ........ Public API ................*/
289   get onceInitialized() {
290     return gOnceInitializedDeferred.promise;
291   },
293   /**
294    * Get the session state as a jsval
295    */
296   get state() {
297     return this._initialState;
298   },
300   /**
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.
305    * @returns bool
306    */
307   isAutomaticRestoreEnabled() {
308     if (this._resumeSessionEnabled === null) {
309       this._resumeSessionEnabled =
310         !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing &&
311         (Services.prefs.getBoolPref(
312           "browser.sessionstore.resume_session_once"
313         ) ||
314           Services.prefs.getIntPref("browser.startup.page") ==
315             BROWSER_STARTUP_RESUME_SESSION);
316     }
318     return this._resumeSessionEnabled;
319   },
321   /**
322    * Determines whether there is a pending session restore.
323    * @returns bool
324    */
325   willRestore() {
326     return (
327       this.sessionType == this.RECOVER_SESSION ||
328       this.sessionType == this.RESUME_SESSION
329     );
330   },
332   /**
333    * Determines whether there is a pending session restore and if that will refer
334    * back to a crash.
335    * @returns bool
336    */
337   willRestoreAsCrashed() {
338     return this.sessionType == this.RECOVER_SESSION;
339   },
341   /**
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.
348    */
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()) {
356       return false;
357     }
358     // If we've already restored the session, we won't override again.
359     if (this._didRestore) {
360       return false;
361     }
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.
367         resolve(
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))
375         );
376       });
377     });
378   },
380   /**
381    * Get the type of pending session store, if any.
382    */
383   get sessionType() {
384     if (this._sessionType === null) {
385       let resumeFromCrash = Services.prefs.getBoolPref(
386         "browser.sessionstore.resume_from_crash"
387       );
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;
395       } else {
396         this._sessionType = this.NO_SESSION;
397       }
398     }
400     return this._sessionType;
401   },
403   /**
404    * Get whether the previous session crashed.
405    */
406   get previousSessionCrashed() {
407     return this._previousSessionCrashed;
408   },
410   resetForTest() {
411     this._resumeSessionEnabled = null;
412     this._sessionType = null;
413   },
415   QueryInterface: ChromeUtils.generateQI([
416     "nsIObserver",
417     "nsISupportsWeakReference",
418   ]),