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 };