|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["SessionSaver"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 |
|
13 Cu.import("resource://gre/modules/Timer.jsm", this); |
|
14 Cu.import("resource://gre/modules/Services.jsm", this); |
|
15 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
16 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); |
|
17 |
|
18 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
19 "resource://gre/modules/devtools/Console.jsm"); |
|
20 XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", |
|
21 "resource:///modules/sessionstore/PrivacyFilter.jsm"); |
|
22 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", |
|
23 "resource:///modules/sessionstore/SessionStore.jsm"); |
|
24 XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", |
|
25 "resource:///modules/sessionstore/SessionFile.jsm"); |
|
26 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
27 "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
28 |
|
29 // Minimal interval between two save operations (in milliseconds). |
|
30 XPCOMUtils.defineLazyGetter(this, "gInterval", function () { |
|
31 const PREF = "browser.sessionstore.interval"; |
|
32 |
|
33 // Observer that updates the cached value when the preference changes. |
|
34 Services.prefs.addObserver(PREF, () => { |
|
35 this.gInterval = Services.prefs.getIntPref(PREF); |
|
36 |
|
37 // Cancel any pending runs and call runDelayed() with |
|
38 // zero to apply the newly configured interval. |
|
39 SessionSaverInternal.cancel(); |
|
40 SessionSaverInternal.runDelayed(0); |
|
41 }, false); |
|
42 |
|
43 return Services.prefs.getIntPref(PREF); |
|
44 }); |
|
45 |
|
46 // Notify observers about a given topic with a given subject. |
|
47 function notify(subject, topic) { |
|
48 Services.obs.notifyObservers(subject, topic, ""); |
|
49 } |
|
50 |
|
51 // TelemetryStopwatch helper functions. |
|
52 function stopWatch(method) { |
|
53 return function (...histograms) { |
|
54 for (let hist of histograms) { |
|
55 TelemetryStopwatch[method]("FX_SESSION_RESTORE_" + hist); |
|
56 } |
|
57 }; |
|
58 } |
|
59 |
|
60 let stopWatchStart = stopWatch("start"); |
|
61 let stopWatchCancel = stopWatch("cancel"); |
|
62 let stopWatchFinish = stopWatch("finish"); |
|
63 |
|
64 /** |
|
65 * The external API implemented by the SessionSaver module. |
|
66 */ |
|
67 this.SessionSaver = Object.freeze({ |
|
68 /** |
|
69 * Immediately saves the current session to disk. |
|
70 */ |
|
71 run: function () { |
|
72 return SessionSaverInternal.run(); |
|
73 }, |
|
74 |
|
75 /** |
|
76 * Saves the current session to disk delayed by a given amount of time. Should |
|
77 * another delayed run be scheduled already, we will ignore the given delay |
|
78 * and state saving may occur a little earlier. |
|
79 */ |
|
80 runDelayed: function () { |
|
81 SessionSaverInternal.runDelayed(); |
|
82 }, |
|
83 |
|
84 /** |
|
85 * Sets the last save time to the current time. This will cause us to wait for |
|
86 * at least the configured interval when runDelayed() is called next. |
|
87 */ |
|
88 updateLastSaveTime: function () { |
|
89 SessionSaverInternal.updateLastSaveTime(); |
|
90 }, |
|
91 |
|
92 /** |
|
93 * Sets the last save time to zero. This will cause us to |
|
94 * immediately save the next time runDelayed() is called. |
|
95 */ |
|
96 clearLastSaveTime: function () { |
|
97 SessionSaverInternal.clearLastSaveTime(); |
|
98 }, |
|
99 |
|
100 /** |
|
101 * Cancels all pending session saves. |
|
102 */ |
|
103 cancel: function () { |
|
104 SessionSaverInternal.cancel(); |
|
105 } |
|
106 }); |
|
107 |
|
108 /** |
|
109 * The internal API. |
|
110 */ |
|
111 let SessionSaverInternal = { |
|
112 /** |
|
113 * The timeout ID referencing an active timer for a delayed save. When no |
|
114 * save is pending, this is null. |
|
115 */ |
|
116 _timeoutID: null, |
|
117 |
|
118 /** |
|
119 * A timestamp that keeps track of when we saved the session last. We will |
|
120 * this to determine the correct interval between delayed saves to not deceed |
|
121 * the configured session write interval. |
|
122 */ |
|
123 _lastSaveTime: 0, |
|
124 |
|
125 /** |
|
126 * Immediately saves the current session to disk. |
|
127 */ |
|
128 run: function () { |
|
129 return this._saveState(true /* force-update all windows */); |
|
130 }, |
|
131 |
|
132 /** |
|
133 * Saves the current session to disk delayed by a given amount of time. Should |
|
134 * another delayed run be scheduled already, we will ignore the given delay |
|
135 * and state saving may occur a little earlier. |
|
136 * |
|
137 * @param delay (optional) |
|
138 * The minimum delay in milliseconds to wait for until we collect and |
|
139 * save the current session. |
|
140 */ |
|
141 runDelayed: function (delay = 2000) { |
|
142 // Bail out if there's a pending run. |
|
143 if (this._timeoutID) { |
|
144 return; |
|
145 } |
|
146 |
|
147 // Interval until the next disk operation is allowed. |
|
148 delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0); |
|
149 |
|
150 // Schedule a state save. |
|
151 this._timeoutID = setTimeout(() => this._saveStateAsync(), delay); |
|
152 }, |
|
153 |
|
154 /** |
|
155 * Sets the last save time to the current time. This will cause us to wait for |
|
156 * at least the configured interval when runDelayed() is called next. |
|
157 */ |
|
158 updateLastSaveTime: function () { |
|
159 this._lastSaveTime = Date.now(); |
|
160 }, |
|
161 |
|
162 /** |
|
163 * Sets the last save time to zero. This will cause us to |
|
164 * immediately save the next time runDelayed() is called. |
|
165 */ |
|
166 clearLastSaveTime: function () { |
|
167 this._lastSaveTime = 0; |
|
168 }, |
|
169 |
|
170 /** |
|
171 * Cancels all pending session saves. |
|
172 */ |
|
173 cancel: function () { |
|
174 clearTimeout(this._timeoutID); |
|
175 this._timeoutID = null; |
|
176 }, |
|
177 |
|
178 /** |
|
179 * Saves the current session state. Collects data and writes to disk. |
|
180 * |
|
181 * @param forceUpdateAllWindows (optional) |
|
182 * Forces us to recollect data for all windows and will bypass and |
|
183 * update the corresponding caches. |
|
184 */ |
|
185 _saveState: function (forceUpdateAllWindows = false) { |
|
186 // Cancel any pending timeouts. |
|
187 this.cancel(); |
|
188 |
|
189 if (PrivateBrowsingUtils.permanentPrivateBrowsing) { |
|
190 // Don't save (or even collect) anything in permanent private |
|
191 // browsing mode |
|
192 |
|
193 this.updateLastSaveTime(); |
|
194 return Promise.resolve(); |
|
195 } |
|
196 |
|
197 stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS"); |
|
198 let state = SessionStore.getCurrentState(forceUpdateAllWindows); |
|
199 PrivacyFilter.filterPrivateWindowsAndTabs(state); |
|
200 |
|
201 // Make sure that we keep the previous session if we started with a single |
|
202 // private window and no non-private windows have been opened, yet. |
|
203 if (state.deferredInitialState) { |
|
204 state.windows = state.deferredInitialState.windows || []; |
|
205 delete state.deferredInitialState; |
|
206 } |
|
207 |
|
208 #ifndef XP_MACOSX |
|
209 // We want to restore closed windows that are marked with _shouldRestore. |
|
210 // We're doing this here because we want to control this only when saving |
|
211 // the file. |
|
212 while (state._closedWindows.length) { |
|
213 let i = state._closedWindows.length - 1; |
|
214 |
|
215 if (!state._closedWindows[i]._shouldRestore) { |
|
216 // We only need to go until _shouldRestore |
|
217 // is falsy since we're going in reverse. |
|
218 break; |
|
219 } |
|
220 |
|
221 delete state._closedWindows[i]._shouldRestore; |
|
222 state.windows.unshift(state._closedWindows.pop()); |
|
223 } |
|
224 #endif |
|
225 |
|
226 stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS"); |
|
227 return this._writeState(state); |
|
228 }, |
|
229 |
|
230 /** |
|
231 * Saves the current session state. Collects data asynchronously and calls |
|
232 * _saveState() to collect data again (with a cache hit rate of hopefully |
|
233 * 100%) and write to disk afterwards. |
|
234 */ |
|
235 _saveStateAsync: function () { |
|
236 // Allow scheduling delayed saves again. |
|
237 this._timeoutID = null; |
|
238 |
|
239 // Write to disk. |
|
240 this._saveState(); |
|
241 }, |
|
242 |
|
243 /** |
|
244 * Write the given state object to disk. |
|
245 */ |
|
246 _writeState: function (state) { |
|
247 // Inform observers |
|
248 notify(null, "sessionstore-state-write"); |
|
249 |
|
250 stopWatchStart("SERIALIZE_DATA_MS", "SERIALIZE_DATA_LONGEST_OP_MS", "WRITE_STATE_LONGEST_OP_MS"); |
|
251 let data = JSON.stringify(state); |
|
252 stopWatchFinish("SERIALIZE_DATA_MS", "SERIALIZE_DATA_LONGEST_OP_MS"); |
|
253 |
|
254 // We update the time stamp before writing so that we don't write again |
|
255 // too soon, if saving is requested before the write completes. Without |
|
256 // this update we may save repeatedly if actions cause a runDelayed |
|
257 // before writing has completed. See Bug 902280 |
|
258 this.updateLastSaveTime(); |
|
259 |
|
260 // Write (atomically) to a session file, using a tmp file. Once the session |
|
261 // file is successfully updated, save the time stamp of the last save and |
|
262 // notify the observers. |
|
263 stopWatchStart("SEND_SERIALIZED_STATE_LONGEST_OP_MS"); |
|
264 let promise = SessionFile.write(data); |
|
265 stopWatchFinish("WRITE_STATE_LONGEST_OP_MS", |
|
266 "SEND_SERIALIZED_STATE_LONGEST_OP_MS"); |
|
267 promise = promise.then(() => { |
|
268 this.updateLastSaveTime(); |
|
269 notify(null, "sessionstore-state-write-complete"); |
|
270 }, console.error); |
|
271 |
|
272 return promise; |
|
273 }, |
|
274 }; |