|
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 "use strict"; |
|
6 |
|
7 /** |
|
8 * Session Storage and Restoration |
|
9 * |
|
10 * Overview |
|
11 * This service reads user's session file at startup, and makes a determination |
|
12 * as to whether the session should be restored. It will restore the session |
|
13 * under the circumstances described below. If the auto-start Private Browsing |
|
14 * mode is active, however, the session is never restored. |
|
15 * |
|
16 * Crash Detection |
|
17 * The CrashMonitor is used to check if the final session state was successfully |
|
18 * written at shutdown of the last session. If we did not reach |
|
19 * 'sessionstore-final-state-write-complete', then it's assumed that the browser |
|
20 * has previously crashed and we should restore the session. |
|
21 * |
|
22 * Forced Restarts |
|
23 * In the event that a restart is required due to application update or extension |
|
24 * installation, set the browser.sessionstore.resume_session_once pref to true, |
|
25 * and the session will be restored the next time the browser starts. |
|
26 * |
|
27 * Always Resume |
|
28 * This service will always resume the session if the integer pref |
|
29 * browser.startup.page is set to 3. |
|
30 */ |
|
31 |
|
32 /* :::::::: Constants and Helpers ::::::::::::::: */ |
|
33 |
|
34 const Cc = Components.classes; |
|
35 const Ci = Components.interfaces; |
|
36 const Cr = Components.results; |
|
37 const Cu = Components.utils; |
|
38 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
39 Cu.import("resource://gre/modules/Services.jsm"); |
|
40 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); |
|
41 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
42 Cu.import("resource://gre/modules/Promise.jsm"); |
|
43 |
|
44 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
45 "resource://gre/modules/devtools/Console.jsm"); |
|
46 XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", |
|
47 "resource:///modules/sessionstore/SessionFile.jsm"); |
|
48 XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor", |
|
49 "resource://gre/modules/CrashMonitor.jsm"); |
|
50 |
|
51 const STATE_RUNNING_STR = "running"; |
|
52 |
|
53 // 'browser.startup.page' preference value to resume the previous session. |
|
54 const BROWSER_STARTUP_RESUME_SESSION = 3; |
|
55 |
|
56 function debug(aMsg) { |
|
57 aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n"); |
|
58 Services.console.logStringMessage(aMsg); |
|
59 } |
|
60 |
|
61 let gOnceInitializedDeferred = Promise.defer(); |
|
62 |
|
63 /* :::::::: The Service ::::::::::::::: */ |
|
64 |
|
65 function SessionStartup() { |
|
66 } |
|
67 |
|
68 SessionStartup.prototype = { |
|
69 |
|
70 // the state to restore at startup |
|
71 _initialState: null, |
|
72 _sessionType: Ci.nsISessionStartup.NO_SESSION, |
|
73 _initialized: false, |
|
74 |
|
75 // Stores whether the previous session crashed. |
|
76 _previousSessionCrashed: null, |
|
77 |
|
78 /* ........ Global Event Handlers .............. */ |
|
79 |
|
80 /** |
|
81 * Initialize the component |
|
82 */ |
|
83 init: function sss_init() { |
|
84 Services.obs.notifyObservers(null, "sessionstore-init-started", null); |
|
85 |
|
86 // do not need to initialize anything in auto-started private browsing sessions |
|
87 if (PrivateBrowsingUtils.permanentPrivateBrowsing) { |
|
88 this._initialized = true; |
|
89 gOnceInitializedDeferred.resolve(); |
|
90 return; |
|
91 } |
|
92 |
|
93 SessionFile.read().then( |
|
94 this._onSessionFileRead.bind(this), |
|
95 console.error |
|
96 ); |
|
97 }, |
|
98 |
|
99 // Wrap a string as a nsISupports |
|
100 _createSupportsString: function ssfi_createSupportsString(aData) { |
|
101 let string = Cc["@mozilla.org/supports-string;1"] |
|
102 .createInstance(Ci.nsISupportsString); |
|
103 string.data = aData; |
|
104 return string; |
|
105 }, |
|
106 |
|
107 /** |
|
108 * Complete initialization once the Session File has been read |
|
109 * |
|
110 * @param stateString |
|
111 * string The Session State string read from disk |
|
112 */ |
|
113 _onSessionFileRead: function (stateString) { |
|
114 this._initialized = true; |
|
115 |
|
116 // Let observers modify the state before it is used |
|
117 let supportsStateString = this._createSupportsString(stateString); |
|
118 Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", ""); |
|
119 stateString = supportsStateString.data; |
|
120 |
|
121 // No valid session found. |
|
122 if (!stateString) { |
|
123 this._sessionType = Ci.nsISessionStartup.NO_SESSION; |
|
124 Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); |
|
125 gOnceInitializedDeferred.resolve(); |
|
126 return; |
|
127 } |
|
128 |
|
129 this._initialState = this._parseStateString(stateString); |
|
130 |
|
131 let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once"); |
|
132 let shouldResumeSession = shouldResumeSessionOnce || |
|
133 Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; |
|
134 |
|
135 // If this is a normal restore then throw away any previous session |
|
136 if (!shouldResumeSessionOnce && this._initialState) { |
|
137 delete this._initialState.lastSessionState; |
|
138 } |
|
139 |
|
140 let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"); |
|
141 |
|
142 CrashMonitor.previousCheckpoints.then(checkpoints => { |
|
143 if (checkpoints) { |
|
144 // If the previous session finished writing the final state, we'll |
|
145 // assume there was no crash. |
|
146 this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"]; |
|
147 } else { |
|
148 // If the Crash Monitor could not load a checkpoints file it will |
|
149 // provide null. This could occur on the first run after updating to |
|
150 // a version including the Crash Monitor, or if the checkpoints file |
|
151 // was removed. |
|
152 // |
|
153 // If this is the first run after an update, sessionstore.js should |
|
154 // still contain the session.state flag to indicate if the session |
|
155 // crashed. If it is not present, we will assume this was not the first |
|
156 // run after update and the checkpoints file was somehow corrupted or |
|
157 // removed by a crash. |
|
158 // |
|
159 // If the session.state flag is present, we will fallback to using it |
|
160 // for crash detection - If the last write of sessionstore.js had it |
|
161 // set to "running", we crashed. |
|
162 let stateFlagPresent = (this._initialState && |
|
163 this._initialState.session && |
|
164 this._initialState.session.state); |
|
165 |
|
166 |
|
167 this._previousSessionCrashed = !stateFlagPresent || |
|
168 (this._initialState.session.state == STATE_RUNNING_STR); |
|
169 } |
|
170 |
|
171 // Report shutdown success via telemetry. Shortcoming here are |
|
172 // being-killed-by-OS-shutdown-logic, shutdown freezing after |
|
173 // session restore was written, etc. |
|
174 Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!this._previousSessionCrashed); |
|
175 |
|
176 // set the startup type |
|
177 if (this._previousSessionCrashed && resumeFromCrash) |
|
178 this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION; |
|
179 else if (!this._previousSessionCrashed && shouldResumeSession) |
|
180 this._sessionType = Ci.nsISessionStartup.RESUME_SESSION; |
|
181 else if (this._initialState) |
|
182 this._sessionType = Ci.nsISessionStartup.DEFER_SESSION; |
|
183 else |
|
184 this._initialState = null; // reset the state |
|
185 |
|
186 Services.obs.addObserver(this, "sessionstore-windows-restored", true); |
|
187 |
|
188 if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) |
|
189 Services.obs.addObserver(this, "browser:purge-session-history", true); |
|
190 |
|
191 // We're ready. Notify everyone else. |
|
192 Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); |
|
193 gOnceInitializedDeferred.resolve(); |
|
194 }); |
|
195 }, |
|
196 |
|
197 |
|
198 /** |
|
199 * Convert the Session State string into a state object |
|
200 * |
|
201 * @param stateString |
|
202 * string The Session State string read from disk |
|
203 * @returns {State} a Session State object |
|
204 */ |
|
205 _parseStateString: function (stateString) { |
|
206 let state = null; |
|
207 let corruptFile = false; |
|
208 |
|
209 try { |
|
210 state = JSON.parse(stateString); |
|
211 } catch (ex) { |
|
212 debug("The session file contained un-parse-able JSON: " + ex); |
|
213 corruptFile = true; |
|
214 } |
|
215 Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile); |
|
216 |
|
217 return state; |
|
218 }, |
|
219 |
|
220 /** |
|
221 * Handle notifications |
|
222 */ |
|
223 observe: function sss_observe(aSubject, aTopic, aData) { |
|
224 switch (aTopic) { |
|
225 case "app-startup": |
|
226 Services.obs.addObserver(this, "final-ui-startup", true); |
|
227 Services.obs.addObserver(this, "quit-application", true); |
|
228 break; |
|
229 case "final-ui-startup": |
|
230 Services.obs.removeObserver(this, "final-ui-startup"); |
|
231 Services.obs.removeObserver(this, "quit-application"); |
|
232 this.init(); |
|
233 break; |
|
234 case "quit-application": |
|
235 // no reason for initializing at this point (cf. bug 409115) |
|
236 Services.obs.removeObserver(this, "final-ui-startup"); |
|
237 Services.obs.removeObserver(this, "quit-application"); |
|
238 if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) |
|
239 Services.obs.removeObserver(this, "browser:purge-session-history"); |
|
240 break; |
|
241 case "sessionstore-windows-restored": |
|
242 Services.obs.removeObserver(this, "sessionstore-windows-restored"); |
|
243 // free _initialState after nsSessionStore is done with it |
|
244 this._initialState = null; |
|
245 break; |
|
246 case "browser:purge-session-history": |
|
247 Services.obs.removeObserver(this, "browser:purge-session-history"); |
|
248 // reset all state on sanitization |
|
249 this._sessionType = Ci.nsISessionStartup.NO_SESSION; |
|
250 break; |
|
251 } |
|
252 }, |
|
253 |
|
254 /* ........ Public API ................*/ |
|
255 |
|
256 get onceInitialized() { |
|
257 return gOnceInitializedDeferred.promise; |
|
258 }, |
|
259 |
|
260 /** |
|
261 * Get the session state as a jsval |
|
262 */ |
|
263 get state() { |
|
264 this._ensureInitialized(); |
|
265 return this._initialState; |
|
266 }, |
|
267 |
|
268 /** |
|
269 * Determines whether there is a pending session restore. Should only be |
|
270 * called after initialization has completed. |
|
271 * @throws Error if initialization is not complete yet. |
|
272 * @returns bool |
|
273 */ |
|
274 doRestore: function sss_doRestore() { |
|
275 this._ensureInitialized(); |
|
276 return this._willRestore(); |
|
277 }, |
|
278 |
|
279 /** |
|
280 * Determines whether automatic session restoration is enabled for this |
|
281 * launch of the browser. This does not include crash restoration. In |
|
282 * particular, if session restore is configured to restore only in case of |
|
283 * crash, this method returns false. |
|
284 * @returns bool |
|
285 */ |
|
286 isAutomaticRestoreEnabled: function () { |
|
287 return Services.prefs.getBoolPref("browser.sessionstore.resume_session_once") || |
|
288 Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION; |
|
289 }, |
|
290 |
|
291 /** |
|
292 * Determines whether there is a pending session restore. |
|
293 * @returns bool |
|
294 */ |
|
295 _willRestore: function () { |
|
296 return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || |
|
297 this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; |
|
298 }, |
|
299 |
|
300 /** |
|
301 * Returns whether we will restore a session that ends up replacing the |
|
302 * homepage. The browser uses this to not start loading the homepage if |
|
303 * we're going to stop its load anyway shortly after. |
|
304 * |
|
305 * This is meant to be an optimization for the average case that loading the |
|
306 * session file finishes before we may want to start loading the default |
|
307 * homepage. Should this be called before the session file has been read it |
|
308 * will just return false. |
|
309 * |
|
310 * @returns bool |
|
311 */ |
|
312 get willOverrideHomepage() { |
|
313 if (this._initialState && this._willRestore()) { |
|
314 let windows = this._initialState.windows || null; |
|
315 // If there are valid windows with not only pinned tabs, signal that we |
|
316 // will override the default homepage by restoring a session. |
|
317 return windows && windows.some(w => w.tabs.some(t => !t.pinned)); |
|
318 } |
|
319 return false; |
|
320 }, |
|
321 |
|
322 /** |
|
323 * Get the type of pending session store, if any. |
|
324 */ |
|
325 get sessionType() { |
|
326 this._ensureInitialized(); |
|
327 return this._sessionType; |
|
328 }, |
|
329 |
|
330 /** |
|
331 * Get whether the previous session crashed. |
|
332 */ |
|
333 get previousSessionCrashed() { |
|
334 this._ensureInitialized(); |
|
335 return this._previousSessionCrashed; |
|
336 }, |
|
337 |
|
338 // Ensure that initialization is complete. If initialization is not complete |
|
339 // yet, something is attempting to use the old synchronous initialization, |
|
340 // throw an error. |
|
341 _ensureInitialized: function sss__ensureInitialized() { |
|
342 if (!this._initialized) { |
|
343 throw new Error("Session Store is not initialized."); |
|
344 } |
|
345 }, |
|
346 |
|
347 /* ........ QueryInterface .............. */ |
|
348 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, |
|
349 Ci.nsISupportsWeakReference, |
|
350 Ci.nsISessionStartup]), |
|
351 classID: Components.ID("{ec7a6c20-e081-11da-8ad9-0800200c9a66}") |
|
352 }; |
|
353 |
|
354 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStartup]); |