1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/sessionstore/src/nsSessionStartup.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,354 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +/** 1.11 + * Session Storage and Restoration 1.12 + * 1.13 + * Overview 1.14 + * This service reads user's session file at startup, and makes a determination 1.15 + * as to whether the session should be restored. It will restore the session 1.16 + * under the circumstances described below. If the auto-start Private Browsing 1.17 + * mode is active, however, the session is never restored. 1.18 + * 1.19 + * Crash Detection 1.20 + * The CrashMonitor is used to check if the final session state was successfully 1.21 + * written at shutdown of the last session. If we did not reach 1.22 + * 'sessionstore-final-state-write-complete', then it's assumed that the browser 1.23 + * has previously crashed and we should restore the session. 1.24 + * 1.25 + * Forced Restarts 1.26 + * In the event that a restart is required due to application update or extension 1.27 + * installation, set the browser.sessionstore.resume_session_once pref to true, 1.28 + * and the session will be restored the next time the browser starts. 1.29 + * 1.30 + * Always Resume 1.31 + * This service will always resume the session if the integer pref 1.32 + * browser.startup.page is set to 3. 1.33 + */ 1.34 + 1.35 +/* :::::::: Constants and Helpers ::::::::::::::: */ 1.36 + 1.37 +const Cc = Components.classes; 1.38 +const Ci = Components.interfaces; 1.39 +const Cr = Components.results; 1.40 +const Cu = Components.utils; 1.41 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.42 +Cu.import("resource://gre/modules/Services.jsm"); 1.43 +Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); 1.44 +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.45 +Cu.import("resource://gre/modules/Promise.jsm"); 1.46 + 1.47 +XPCOMUtils.defineLazyModuleGetter(this, "console", 1.48 + "resource://gre/modules/devtools/Console.jsm"); 1.49 +XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", 1.50 + "resource:///modules/sessionstore/SessionFile.jsm"); 1.51 +XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor", 1.52 + "resource://gre/modules/CrashMonitor.jsm"); 1.53 + 1.54 +const STATE_RUNNING_STR = "running"; 1.55 + 1.56 +// 'browser.startup.page' preference value to resume the previous session. 1.57 +const BROWSER_STARTUP_RESUME_SESSION = 3; 1.58 + 1.59 +function debug(aMsg) { 1.60 + aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n"); 1.61 + Services.console.logStringMessage(aMsg); 1.62 +} 1.63 + 1.64 +let gOnceInitializedDeferred = Promise.defer(); 1.65 + 1.66 +/* :::::::: The Service ::::::::::::::: */ 1.67 + 1.68 +function SessionStartup() { 1.69 +} 1.70 + 1.71 +SessionStartup.prototype = { 1.72 + 1.73 + // the state to restore at startup 1.74 + _initialState: null, 1.75 + _sessionType: Ci.nsISessionStartup.NO_SESSION, 1.76 + _initialized: false, 1.77 + 1.78 + // Stores whether the previous session crashed. 1.79 + _previousSessionCrashed: null, 1.80 + 1.81 +/* ........ Global Event Handlers .............. */ 1.82 + 1.83 + /** 1.84 + * Initialize the component 1.85 + */ 1.86 + init: function sss_init() { 1.87 + Services.obs.notifyObservers(null, "sessionstore-init-started", null); 1.88 + 1.89 + // do not need to initialize anything in auto-started private browsing sessions 1.90 + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { 1.91 + this._initialized = true; 1.92 + gOnceInitializedDeferred.resolve(); 1.93 + return; 1.94 + } 1.95 + 1.96 + SessionFile.read().then( 1.97 + this._onSessionFileRead.bind(this), 1.98 + console.error 1.99 + ); 1.100 + }, 1.101 + 1.102 + // Wrap a string as a nsISupports 1.103 + _createSupportsString: function ssfi_createSupportsString(aData) { 1.104 + let string = Cc["@mozilla.org/supports-string;1"] 1.105 + .createInstance(Ci.nsISupportsString); 1.106 + string.data = aData; 1.107 + return string; 1.108 + }, 1.109 + 1.110 + /** 1.111 + * Complete initialization once the Session File has been read 1.112 + * 1.113 + * @param stateString 1.114 + * string The Session State string read from disk 1.115 + */ 1.116 + _onSessionFileRead: function (stateString) { 1.117 + this._initialized = true; 1.118 + 1.119 + // Let observers modify the state before it is used 1.120 + let supportsStateString = this._createSupportsString(stateString); 1.121 + Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", ""); 1.122 + stateString = supportsStateString.data; 1.123 + 1.124 + // No valid session found. 1.125 + if (!stateString) { 1.126 + this._sessionType = Ci.nsISessionStartup.NO_SESSION; 1.127 + Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); 1.128 + gOnceInitializedDeferred.resolve(); 1.129 + return; 1.130 + } 1.131 + 1.132 + this._initialState = this._parseStateString(stateString); 1.133 + 1.134 + let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); 1.135 + let shouldResumeSession = shouldResumeSessionOnce || 1.136 + Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; 1.137 + 1.138 + // If this is a normal restore then throw away any previous session 1.139 + if (!shouldResumeSessionOnce && this._initialState) { 1.140 + delete this._initialState.lastSessionState; 1.141 + } 1.142 + 1.143 + let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"); 1.144 + 1.145 + CrashMonitor.previousCheckpoints.then(checkpoints => { 1.146 + if (checkpoints) { 1.147 + // If the previous session finished writing the final state, we'll 1.148 + // assume there was no crash. 1.149 + this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"]; 1.150 + } else { 1.151 + // If the Crash Monitor could not load a checkpoints file it will 1.152 + // provide null. This could occur on the first run after updating to 1.153 + // a version including the Crash Monitor, or if the checkpoints file 1.154 + // was removed. 1.155 + // 1.156 + // If this is the first run after an update, sessionstore.js should 1.157 + // still contain the session.state flag to indicate if the session 1.158 + // crashed. If it is not present, we will assume this was not the first 1.159 + // run after update and the checkpoints file was somehow corrupted or 1.160 + // removed by a crash. 1.161 + // 1.162 + // If the session.state flag is present, we will fallback to using it 1.163 + // for crash detection - If the last write of sessionstore.js had it 1.164 + // set to "running", we crashed. 1.165 + let stateFlagPresent = (this._initialState && 1.166 + this._initialState.session && 1.167 + this._initialState.session.state); 1.168 + 1.169 + 1.170 + this._previousSessionCrashed = !stateFlagPresent || 1.171 + (this._initialState.session.state == STATE_RUNNING_STR); 1.172 + } 1.173 + 1.174 + // Report shutdown success via telemetry. Shortcoming here are 1.175 + // being-killed-by-OS-shutdown-logic, shutdown freezing after 1.176 + // session restore was written, etc. 1.177 + Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed); 1.178 + 1.179 + // set the startup type 1.180 + if (this._previousSessionCrashed && resumeFromCrash) 1.181 + this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION; 1.182 + else if (!this._previousSessionCrashed && shouldResumeSession) 1.183 + this._sessionType = Ci.nsISessionStartup.RESUME_SESSION; 1.184 + else if (this._initialState) 1.185 + this._sessionType = Ci.nsISessionStartup.DEFER_SESSION; 1.186 + else 1.187 + this._initialState = null; // reset the state 1.188 + 1.189 + Services.obs.addObserver(this, "sessionstore-windows-restored", true); 1.190 + 1.191 + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) 1.192 + Services.obs.addObserver(this, "browser:purge-session-history", true); 1.193 + 1.194 + // We're ready. Notify everyone else. 1.195 + Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); 1.196 + gOnceInitializedDeferred.resolve(); 1.197 + }); 1.198 + }, 1.199 + 1.200 + 1.201 + /** 1.202 + * Convert the Session State string into a state object 1.203 + * 1.204 + * @param stateString 1.205 + * string The Session State string read from disk 1.206 + * @returns {State} a Session State object 1.207 + */ 1.208 + _parseStateString: function (stateString) { 1.209 + let state = null; 1.210 + let corruptFile = false; 1.211 + 1.212 + try { 1.213 + state = JSON.parse(stateString); 1.214 + } catch (ex) { 1.215 + debug("The session file contained un-parse-able JSON: " + ex); 1.216 + corruptFile = true; 1.217 + } 1.218 + Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile); 1.219 + 1.220 + return state; 1.221 + }, 1.222 + 1.223 + /** 1.224 + * Handle notifications 1.225 + */ 1.226 + observe: function sss_observe(aSubject, aTopic, aData) { 1.227 + switch (aTopic) { 1.228 + case "app-startup": 1.229 + Services.obs.addObserver(this, "final-ui-startup", true); 1.230 + Services.obs.addObserver(this, "quit-application", true); 1.231 + break; 1.232 + case "final-ui-startup": 1.233 + Services.obs.removeObserver(this, "final-ui-startup"); 1.234 + Services.obs.removeObserver(this, "quit-application"); 1.235 + this.init(); 1.236 + break; 1.237 + case "quit-application": 1.238 + // no reason for initializing at this point (cf. bug 409115) 1.239 + Services.obs.removeObserver(this, "final-ui-startup"); 1.240 + Services.obs.removeObserver(this, "quit-application"); 1.241 + if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) 1.242 + Services.obs.removeObserver(this, "browser:purge-session-history"); 1.243 + break; 1.244 + case "sessionstore-windows-restored": 1.245 + Services.obs.removeObserver(this, "sessionstore-windows-restored"); 1.246 + // free _initialState after nsSessionStore is done with it 1.247 + this._initialState = null; 1.248 + break; 1.249 + case "browser:purge-session-history": 1.250 + Services.obs.removeObserver(this, "browser:purge-session-history"); 1.251 + // reset all state on sanitization 1.252 + this._sessionType = Ci.nsISessionStartup.NO_SESSION; 1.253 + break; 1.254 + } 1.255 + }, 1.256 + 1.257 +/* ........ Public API ................*/ 1.258 + 1.259 + get onceInitialized() { 1.260 + return gOnceInitializedDeferred.promise; 1.261 + }, 1.262 + 1.263 + /** 1.264 + * Get the session state as a jsval 1.265 + */ 1.266 + get state() { 1.267 + this._ensureInitialized(); 1.268 + return this._initialState; 1.269 + }, 1.270 + 1.271 + /** 1.272 + * Determines whether there is a pending session restore. Should only be 1.273 + * called after initialization has completed. 1.274 + * @throws Error if initialization is not complete yet. 1.275 + * @returns bool 1.276 + */ 1.277 + doRestore: function sss_doRestore() { 1.278 + this._ensureInitialized(); 1.279 + return this._willRestore(); 1.280 + }, 1.281 + 1.282 + /** 1.283 + * Determines whether automatic session restoration is enabled for this 1.284 + * launch of the browser. This does not include crash restoration. In 1.285 + * particular, if session restore is configured to restore only in case of 1.286 + * crash, this method returns false. 1.287 + * @returns bool 1.288 + */ 1.289 + isAutomaticRestoreEnabled: function () { 1.290 + return Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") || 1.291 + Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; 1.292 + }, 1.293 + 1.294 + /** 1.295 + * Determines whether there is a pending session restore. 1.296 + * @returns bool 1.297 + */ 1.298 + _willRestore: function () { 1.299 + return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || 1.300 + this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; 1.301 + }, 1.302 + 1.303 + /** 1.304 + * Returns whether we will restore a session that ends up replacing the 1.305 + * homepage. The browser uses this to not start loading the homepage if 1.306 + * we're going to stop its load anyway shortly after. 1.307 + * 1.308 + * This is meant to be an optimization for the average case that loading the 1.309 + * session file finishes before we may want to start loading the default 1.310 + * homepage. Should this be called before the session file has been read it 1.311 + * will just return false. 1.312 + * 1.313 + * @returns bool 1.314 + */ 1.315 + get willOverrideHomepage() { 1.316 + if (this._initialState && this._willRestore()) { 1.317 + let windows = this._initialState.windows || null; 1.318 + // If there are valid windows with not only pinned tabs, signal that we 1.319 + // will override the default homepage by restoring a session. 1.320 + return windows && windows.some(w => w.tabs.some(t => !t.pinned)); 1.321 + } 1.322 + return false; 1.323 + }, 1.324 + 1.325 + /** 1.326 + * Get the type of pending session store, if any. 1.327 + */ 1.328 + get sessionType() { 1.329 + this._ensureInitialized(); 1.330 + return this._sessionType; 1.331 + }, 1.332 + 1.333 + /** 1.334 + * Get whether the previous session crashed. 1.335 + */ 1.336 + get previousSessionCrashed() { 1.337 + this._ensureInitialized(); 1.338 + return this._previousSessionCrashed; 1.339 + }, 1.340 + 1.341 + // Ensure that initialization is complete. If initialization is not complete 1.342 + // yet, something is attempting to use the old synchronous initialization, 1.343 + // throw an error. 1.344 + _ensureInitialized: function sss__ensureInitialized() { 1.345 + if (!this._initialized) { 1.346 + throw new Error("Session Store is not initialized."); 1.347 + } 1.348 + }, 1.349 + 1.350 + /* ........ QueryInterface .............. */ 1.351 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 1.352 + Ci.nsISupportsWeakReference, 1.353 + Ci.nsISessionStartup]), 1.354 + classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}") 1.355 +}; 1.356 + 1.357 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]);