Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | const Cc = Components.classes; |
michael@0 | 6 | const Ci = Components.interfaces; |
michael@0 | 7 | const Cu = Components.utils; |
michael@0 | 8 | const Cr = Components.results; |
michael@0 | 9 | |
michael@0 | 10 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 11 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 12 | Cu.import("resource://gre/modules/WindowsPrefSync.jsm"); |
michael@0 | 13 | |
michael@0 | 14 | #ifdef MOZ_CRASHREPORTER |
michael@0 | 15 | XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", |
michael@0 | 16 | "@mozilla.org/xre/app-info;1", "nsICrashReporter"); |
michael@0 | 17 | #endif |
michael@0 | 18 | |
michael@0 | 19 | XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor", |
michael@0 | 20 | "resource://gre/modules/CrashMonitor.jsm"); |
michael@0 | 21 | |
michael@0 | 22 | XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator", |
michael@0 | 23 | "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); |
michael@0 | 24 | |
michael@0 | 25 | XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { |
michael@0 | 26 | Cu.import("resource://gre/modules/NetUtil.jsm"); |
michael@0 | 27 | return NetUtil; |
michael@0 | 28 | }); |
michael@0 | 29 | |
michael@0 | 30 | XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", |
michael@0 | 31 | "resource://gre/modules/UITelemetry.jsm"); |
michael@0 | 32 | |
michael@0 | 33 | // ----------------------------------------------------------------------- |
michael@0 | 34 | // Session Store |
michael@0 | 35 | // ----------------------------------------------------------------------- |
michael@0 | 36 | |
michael@0 | 37 | const STATE_STOPPED = 0; |
michael@0 | 38 | const STATE_RUNNING = 1; |
michael@0 | 39 | const STATE_QUITTING = -1; |
michael@0 | 40 | |
michael@0 | 41 | function SessionStore() { } |
michael@0 | 42 | |
michael@0 | 43 | SessionStore.prototype = { |
michael@0 | 44 | classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"), |
michael@0 | 45 | |
michael@0 | 46 | QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, |
michael@0 | 47 | Ci.nsIDOMEventListener, |
michael@0 | 48 | Ci.nsIObserver, |
michael@0 | 49 | Ci.nsISupportsWeakReference]), |
michael@0 | 50 | |
michael@0 | 51 | _windows: {}, |
michael@0 | 52 | _tabsFromOtherGroups: [], |
michael@0 | 53 | _selectedWindow: 1, |
michael@0 | 54 | _orderedWindows: [], |
michael@0 | 55 | _lastSaveTime: 0, |
michael@0 | 56 | _lastSessionTime: 0, |
michael@0 | 57 | _interval: 10000, |
michael@0 | 58 | _maxTabsUndo: 1, |
michael@0 | 59 | _shouldRestore: false, |
michael@0 | 60 | |
michael@0 | 61 | // Tab telemetry variables |
michael@0 | 62 | _maxTabsOpen: 1, |
michael@0 | 63 | |
michael@0 | 64 | init: function ss_init() { |
michael@0 | 65 | // Get file references |
michael@0 | 66 | this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); |
michael@0 | 67 | this._sessionFileBackup = this._sessionFile.clone(); |
michael@0 | 68 | this._sessionCache = this._sessionFile.clone(); |
michael@0 | 69 | this._sessionFile.append("sessionstore.js"); |
michael@0 | 70 | this._sessionFileBackup.append("sessionstore.bak"); |
michael@0 | 71 | this._sessionCache.append("sessionstoreCache"); |
michael@0 | 72 | |
michael@0 | 73 | this._loadState = STATE_STOPPED; |
michael@0 | 74 | |
michael@0 | 75 | try { |
michael@0 | 76 | UITelemetry.addSimpleMeasureFunction("metro-tabs", |
michael@0 | 77 | this._getTabStats.bind(this)); |
michael@0 | 78 | } catch (ex) { |
michael@0 | 79 | // swallow exception that occurs if metro-tabs measure is already set up |
michael@0 | 80 | } |
michael@0 | 81 | |
michael@0 | 82 | CrashMonitor.previousCheckpoints.then(checkpoints => { |
michael@0 | 83 | let previousSessionCrashed = false; |
michael@0 | 84 | |
michael@0 | 85 | if (checkpoints) { |
michael@0 | 86 | // If the previous session finished writing the final state, we'll |
michael@0 | 87 | // assume there was no crash. |
michael@0 | 88 | previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"]; |
michael@0 | 89 | } else { |
michael@0 | 90 | // If no checkpoints are saved, this is the first run with CrashMonitor or the |
michael@0 | 91 | // metroSessionCheckpoints file was corrupted/deleted, so fallback to defining |
michael@0 | 92 | // a crash as init-ing with an unexpected previousExecutionState |
michael@0 | 93 | // 1 == RUNNING, 2 == SUSPENDED |
michael@0 | 94 | previousSessionCrashed = Services.metro.previousExecutionState == 1 || |
michael@0 | 95 | Services.metro.previousExecutionState == 2; |
michael@0 | 96 | } |
michael@0 | 97 | |
michael@0 | 98 | Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!previousSessionCrashed); |
michael@0 | 99 | }); |
michael@0 | 100 | |
michael@0 | 101 | try { |
michael@0 | 102 | let shutdownWasUnclean = false; |
michael@0 | 103 | |
michael@0 | 104 | if (this._sessionFileBackup.exists()) { |
michael@0 | 105 | this._sessionFileBackup.remove(false); |
michael@0 | 106 | shutdownWasUnclean = true; |
michael@0 | 107 | } |
michael@0 | 108 | |
michael@0 | 109 | if (this._sessionFile.exists()) { |
michael@0 | 110 | this._sessionFile.copyTo(null, this._sessionFileBackup.leafName); |
michael@0 | 111 | |
michael@0 | 112 | switch(Services.metro.previousExecutionState) { |
michael@0 | 113 | // 0 == NotRunning |
michael@0 | 114 | case 0: |
michael@0 | 115 | // Disable crash recovery if we have exceeded the timeout |
michael@0 | 116 | this._lastSessionTime = this._sessionFile.lastModifiedTime; |
michael@0 | 117 | let delta = Date.now() - this._lastSessionTime; |
michael@0 | 118 | let timeout = |
michael@0 | 119 | Services.prefs.getIntPref( |
michael@0 | 120 | "browser.sessionstore.resume_from_crash_timeout"); |
michael@0 | 121 | this._shouldRestore = shutdownWasUnclean |
michael@0 | 122 | && (delta < (timeout * 60000)); |
michael@0 | 123 | break; |
michael@0 | 124 | // 1 == Running |
michael@0 | 125 | case 1: |
michael@0 | 126 | // We should never encounter this situation |
michael@0 | 127 | Components.utils.reportError("SessionRestore.init called with " |
michael@0 | 128 | + "previous execution state 'Running'"); |
michael@0 | 129 | this._shouldRestore = true; |
michael@0 | 130 | break; |
michael@0 | 131 | // 2 == Suspended |
michael@0 | 132 | case 2: |
michael@0 | 133 | // We should never encounter this situation |
michael@0 | 134 | Components.utils.reportError("SessionRestore.init called with " |
michael@0 | 135 | + "previous execution state 'Suspended'"); |
michael@0 | 136 | this._shouldRestore = true; |
michael@0 | 137 | break; |
michael@0 | 138 | // 3 == Terminated |
michael@0 | 139 | case 3: |
michael@0 | 140 | // Terminated means that Windows terminated our already-suspended |
michael@0 | 141 | // process to get back some resources. When we re-launch, we want |
michael@0 | 142 | // to provide the illusion that our process was suspended the |
michael@0 | 143 | // whole time, and never terminated. |
michael@0 | 144 | this._shouldRestore = true; |
michael@0 | 145 | break; |
michael@0 | 146 | // 4 == ClosedByUser |
michael@0 | 147 | case 4: |
michael@0 | 148 | // ClosedByUser indicates that the user performed a "close" gesture |
michael@0 | 149 | // on our tile. We should act as if the browser closed normally, |
michael@0 | 150 | // even if we were closed from a suspended state (in which case |
michael@0 | 151 | // we'll have determined that it was an unclean shtudown) |
michael@0 | 152 | this._shouldRestore = false; |
michael@0 | 153 | break; |
michael@0 | 154 | } |
michael@0 | 155 | } |
michael@0 | 156 | |
michael@0 | 157 | if (!this._sessionCache.exists() || !this._sessionCache.isDirectory()) { |
michael@0 | 158 | this._sessionCache.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); |
michael@0 | 159 | } |
michael@0 | 160 | } catch (ex) { |
michael@0 | 161 | Cu.reportError(ex); // file was write-locked? |
michael@0 | 162 | } |
michael@0 | 163 | |
michael@0 | 164 | this._interval = Services.prefs.getIntPref("browser.sessionstore.interval"); |
michael@0 | 165 | this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo"); |
michael@0 | 166 | |
michael@0 | 167 | // Disable crash recovery if it has been turned off |
michael@0 | 168 | if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash")) |
michael@0 | 169 | this._shouldRestore = false; |
michael@0 | 170 | |
michael@0 | 171 | // Do we need to restore session just this once, in case of a restart? |
michael@0 | 172 | if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) { |
michael@0 | 173 | Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false); |
michael@0 | 174 | this._shouldRestore = true; |
michael@0 | 175 | } |
michael@0 | 176 | }, |
michael@0 | 177 | |
michael@0 | 178 | _clearDisk: function ss_clearDisk() { |
michael@0 | 179 | if (this._sessionFile.exists()) { |
michael@0 | 180 | try { |
michael@0 | 181 | this._sessionFile.remove(false); |
michael@0 | 182 | } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? |
michael@0 | 183 | } |
michael@0 | 184 | if (this._sessionFileBackup.exists()) { |
michael@0 | 185 | try { |
michael@0 | 186 | this._sessionFileBackup.remove(false); |
michael@0 | 187 | } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? |
michael@0 | 188 | } |
michael@0 | 189 | |
michael@0 | 190 | this._clearCache(); |
michael@0 | 191 | }, |
michael@0 | 192 | |
michael@0 | 193 | _clearCache: function ss_clearCache() { |
michael@0 | 194 | // First, let's get a list of files we think should be active |
michael@0 | 195 | let activeFiles = []; |
michael@0 | 196 | this._forEachBrowserWindow(function(aWindow) { |
michael@0 | 197 | let tabs = aWindow.Browser.tabs; |
michael@0 | 198 | for (let i = 0; i < tabs.length; i++) { |
michael@0 | 199 | let browser = tabs[i].browser; |
michael@0 | 200 | if (browser.__SS_extdata && "thumbnail" in browser.__SS_extdata) |
michael@0 | 201 | activeFiles.push(browser.__SS_extdata.thumbnail); |
michael@0 | 202 | } |
michael@0 | 203 | }); |
michael@0 | 204 | |
michael@0 | 205 | // Now, let's find the stale files in the cache folder |
michael@0 | 206 | let staleFiles = []; |
michael@0 | 207 | let cacheFiles = this._sessionCache.directoryEntries; |
michael@0 | 208 | while (cacheFiles.hasMoreElements()) { |
michael@0 | 209 | let file = cacheFiles.getNext().QueryInterface(Ci.nsILocalFile); |
michael@0 | 210 | let fileURI = Services.io.newFileURI(file); |
michael@0 | 211 | if (activeFiles.indexOf(fileURI) == -1) |
michael@0 | 212 | staleFiles.push(file); |
michael@0 | 213 | } |
michael@0 | 214 | |
michael@0 | 215 | // Remove the stale files in a separate step to keep the enumerator from |
michael@0 | 216 | // messing up if we remove the files as we collect them. |
michael@0 | 217 | staleFiles.forEach(function(aFile) { |
michael@0 | 218 | aFile.remove(false); |
michael@0 | 219 | }) |
michael@0 | 220 | }, |
michael@0 | 221 | |
michael@0 | 222 | _getTabStats: function() { |
michael@0 | 223 | return { |
michael@0 | 224 | currTabCount: this._currTabCount, |
michael@0 | 225 | maxTabCount: this._maxTabsOpen |
michael@0 | 226 | }; |
michael@0 | 227 | }, |
michael@0 | 228 | |
michael@0 | 229 | observe: function ss_observe(aSubject, aTopic, aData) { |
michael@0 | 230 | let self = this; |
michael@0 | 231 | let observerService = Services.obs; |
michael@0 | 232 | switch (aTopic) { |
michael@0 | 233 | case "app-startup": |
michael@0 | 234 | observerService.addObserver(this, "final-ui-startup", true); |
michael@0 | 235 | observerService.addObserver(this, "domwindowopened", true); |
michael@0 | 236 | observerService.addObserver(this, "domwindowclosed", true); |
michael@0 | 237 | observerService.addObserver(this, "browser-lastwindow-close-granted", true); |
michael@0 | 238 | observerService.addObserver(this, "browser:purge-session-history", true); |
michael@0 | 239 | observerService.addObserver(this, "quit-application-requested", true); |
michael@0 | 240 | observerService.addObserver(this, "quit-application-granted", true); |
michael@0 | 241 | observerService.addObserver(this, "quit-application", true); |
michael@0 | 242 | observerService.addObserver(this, "reset-telemetry-vars", true); |
michael@0 | 243 | break; |
michael@0 | 244 | case "final-ui-startup": |
michael@0 | 245 | observerService.removeObserver(this, "final-ui-startup"); |
michael@0 | 246 | if (WindowsPrefSync) { |
michael@0 | 247 | // Pulls in Desktop controlled prefs and pushes out Metro controlled prefs |
michael@0 | 248 | WindowsPrefSync.init(); |
michael@0 | 249 | } |
michael@0 | 250 | this.init(); |
michael@0 | 251 | break; |
michael@0 | 252 | case "domwindowopened": |
michael@0 | 253 | let window = aSubject; |
michael@0 | 254 | window.addEventListener("load", function() { |
michael@0 | 255 | self.onWindowOpen(window); |
michael@0 | 256 | window.removeEventListener("load", arguments.callee, false); |
michael@0 | 257 | }, false); |
michael@0 | 258 | break; |
michael@0 | 259 | case "domwindowclosed": // catch closed windows |
michael@0 | 260 | this.onWindowClose(aSubject); |
michael@0 | 261 | break; |
michael@0 | 262 | case "browser-lastwindow-close-granted": |
michael@0 | 263 | // If a save has been queued, kill the timer and save state now |
michael@0 | 264 | if (this._saveTimer) { |
michael@0 | 265 | this._saveTimer.cancel(); |
michael@0 | 266 | this._saveTimer = null; |
michael@0 | 267 | this.saveState(); |
michael@0 | 268 | } |
michael@0 | 269 | |
michael@0 | 270 | // Freeze the data at what we've got (ignoring closing windows) |
michael@0 | 271 | this._loadState = STATE_QUITTING; |
michael@0 | 272 | break; |
michael@0 | 273 | case "quit-application-requested": |
michael@0 | 274 | // Get a current snapshot of all windows |
michael@0 | 275 | this._forEachBrowserWindow(function(aWindow) { |
michael@0 | 276 | self._collectWindowData(aWindow); |
michael@0 | 277 | }); |
michael@0 | 278 | break; |
michael@0 | 279 | case "quit-application-granted": |
michael@0 | 280 | // Get a current snapshot of all windows |
michael@0 | 281 | this._forEachBrowserWindow(function(aWindow) { |
michael@0 | 282 | self._collectWindowData(aWindow); |
michael@0 | 283 | }); |
michael@0 | 284 | |
michael@0 | 285 | // Freeze the data at what we've got (ignoring closing windows) |
michael@0 | 286 | this._loadState = STATE_QUITTING; |
michael@0 | 287 | break; |
michael@0 | 288 | case "quit-application": |
michael@0 | 289 | // If we are restarting, lets restore the tabs |
michael@0 | 290 | if (aData == "restart") { |
michael@0 | 291 | Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true); |
michael@0 | 292 | |
michael@0 | 293 | // Ignore purges when restarting. The notification is fired after "quit-application". |
michael@0 | 294 | Services.obs.removeObserver(this, "browser:purge-session-history"); |
michael@0 | 295 | } |
michael@0 | 296 | |
michael@0 | 297 | // Freeze the data at what we've got (ignoring closing windows) |
michael@0 | 298 | this._loadState = STATE_QUITTING; |
michael@0 | 299 | |
michael@0 | 300 | // No need for this back up, we are shutting down just fine |
michael@0 | 301 | if (this._sessionFileBackup.exists()) |
michael@0 | 302 | this._sessionFileBackup.remove(false); |
michael@0 | 303 | |
michael@0 | 304 | observerService.removeObserver(this, "domwindowopened"); |
michael@0 | 305 | observerService.removeObserver(this, "domwindowclosed"); |
michael@0 | 306 | observerService.removeObserver(this, "browser-lastwindow-close-granted"); |
michael@0 | 307 | observerService.removeObserver(this, "quit-application-requested"); |
michael@0 | 308 | observerService.removeObserver(this, "quit-application-granted"); |
michael@0 | 309 | observerService.removeObserver(this, "quit-application"); |
michael@0 | 310 | observerService.removeObserver(this, "reset-telemetry-vars"); |
michael@0 | 311 | |
michael@0 | 312 | // If a save has been queued, kill the timer and save state now |
michael@0 | 313 | if (this._saveTimer) { |
michael@0 | 314 | this._saveTimer.cancel(); |
michael@0 | 315 | this._saveTimer = null; |
michael@0 | 316 | } |
michael@0 | 317 | this.saveState(); |
michael@0 | 318 | break; |
michael@0 | 319 | case "browser:purge-session-history": // catch sanitization |
michael@0 | 320 | this._clearDisk(); |
michael@0 | 321 | |
michael@0 | 322 | // If the browser is shutting down, simply return after clearing the |
michael@0 | 323 | // session data on disk as this notification fires after the |
michael@0 | 324 | // quit-application notification so the browser is about to exit. |
michael@0 | 325 | if (this._loadState == STATE_QUITTING) |
michael@0 | 326 | return; |
michael@0 | 327 | |
michael@0 | 328 | // Clear all data about closed tabs |
michael@0 | 329 | for (let [ssid, win] in Iterator(this._windows)) |
michael@0 | 330 | win._closedTabs = []; |
michael@0 | 331 | |
michael@0 | 332 | if (this._loadState == STATE_RUNNING) { |
michael@0 | 333 | // Save the purged state immediately |
michael@0 | 334 | this.saveStateNow(); |
michael@0 | 335 | } |
michael@0 | 336 | break; |
michael@0 | 337 | case "timer-callback": |
michael@0 | 338 | // Timer call back for delayed saving |
michael@0 | 339 | this._saveTimer = null; |
michael@0 | 340 | this.saveState(); |
michael@0 | 341 | break; |
michael@0 | 342 | case "reset-telemetry-vars": |
michael@0 | 343 | // Used in mochitests only. |
michael@0 | 344 | this._maxTabsOpen = 1; |
michael@0 | 345 | } |
michael@0 | 346 | }, |
michael@0 | 347 | |
michael@0 | 348 | updateTabTelemetryVars: function(window) { |
michael@0 | 349 | this._currTabCount = window.Browser.tabs.length; |
michael@0 | 350 | if (this._currTabCount > this._maxTabsOpen) { |
michael@0 | 351 | this._maxTabsOpen = this._currTabCount; |
michael@0 | 352 | } |
michael@0 | 353 | }, |
michael@0 | 354 | |
michael@0 | 355 | handleEvent: function ss_handleEvent(aEvent) { |
michael@0 | 356 | let window = aEvent.currentTarget.ownerDocument.defaultView; |
michael@0 | 357 | switch (aEvent.type) { |
michael@0 | 358 | case "load": |
michael@0 | 359 | browser = aEvent.currentTarget; |
michael@0 | 360 | if (aEvent.target == browser.contentDocument && browser.__SS_tabFormData) { |
michael@0 | 361 | browser.messageManager.sendAsyncMessage("SessionStore:restoreSessionTabData", { |
michael@0 | 362 | formdata: browser.__SS_tabFormData.formdata, |
michael@0 | 363 | scroll: browser.__SS_tabFormData.scroll |
michael@0 | 364 | }); |
michael@0 | 365 | } |
michael@0 | 366 | break; |
michael@0 | 367 | case "TabOpen": |
michael@0 | 368 | this.updateTabTelemetryVars(window); |
michael@0 | 369 | let browser = aEvent.originalTarget.linkedBrowser; |
michael@0 | 370 | browser.addEventListener("load", this, true); |
michael@0 | 371 | case "TabClose": { |
michael@0 | 372 | let browser = aEvent.originalTarget.linkedBrowser; |
michael@0 | 373 | if (aEvent.type == "TabOpen") { |
michael@0 | 374 | this.onTabAdd(window, browser); |
michael@0 | 375 | } |
michael@0 | 376 | else { |
michael@0 | 377 | this.onTabClose(window, browser); |
michael@0 | 378 | this.onTabRemove(window, browser); |
michael@0 | 379 | } |
michael@0 | 380 | break; |
michael@0 | 381 | } |
michael@0 | 382 | case "TabRemove": |
michael@0 | 383 | this.updateTabTelemetryVars(window); |
michael@0 | 384 | break; |
michael@0 | 385 | case "TabSelect": { |
michael@0 | 386 | let browser = aEvent.originalTarget.linkedBrowser; |
michael@0 | 387 | this.onTabSelect(window, browser); |
michael@0 | 388 | break; |
michael@0 | 389 | } |
michael@0 | 390 | } |
michael@0 | 391 | }, |
michael@0 | 392 | |
michael@0 | 393 | receiveMessage: function ss_receiveMessage(aMessage) { |
michael@0 | 394 | let browser = aMessage.target; |
michael@0 | 395 | switch (aMessage.name) { |
michael@0 | 396 | case "SessionStore:collectFormdata": |
michael@0 | 397 | browser.__SS_data.formdata = aMessage.json.data; |
michael@0 | 398 | break; |
michael@0 | 399 | case "SessionStore:collectScrollPosition": |
michael@0 | 400 | browser.__SS_data.scroll = aMessage.json.data; |
michael@0 | 401 | break; |
michael@0 | 402 | default: |
michael@0 | 403 | let window = aMessage.target.ownerDocument.defaultView; |
michael@0 | 404 | this.onTabLoad(window, aMessage.target, aMessage); |
michael@0 | 405 | break; |
michael@0 | 406 | } |
michael@0 | 407 | }, |
michael@0 | 408 | |
michael@0 | 409 | onWindowOpen: function ss_onWindowOpen(aWindow) { |
michael@0 | 410 | // Return if window has already been initialized |
michael@0 | 411 | if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) |
michael@0 | 412 | return; |
michael@0 | 413 | |
michael@0 | 414 | // Ignore non-browser windows and windows opened while shutting down |
michael@0 | 415 | if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING) |
michael@0 | 416 | return; |
michael@0 | 417 | |
michael@0 | 418 | // Assign it a unique identifier and create its data object |
michael@0 | 419 | aWindow.__SSID = "window" + gUUIDGenerator.generateUUID().toString(); |
michael@0 | 420 | this._windows[aWindow.__SSID] = { tabs: [], selected: 0, _closedTabs: [] }; |
michael@0 | 421 | this._orderedWindows.push(aWindow.__SSID); |
michael@0 | 422 | |
michael@0 | 423 | // Perform additional initialization when the first window is loading |
michael@0 | 424 | if (this._loadState == STATE_STOPPED) { |
michael@0 | 425 | this._loadState = STATE_RUNNING; |
michael@0 | 426 | this._lastSaveTime = Date.now(); |
michael@0 | 427 | |
michael@0 | 428 | // Nothing to restore, notify observers things are complete |
michael@0 | 429 | if (!this.shouldRestore()) { |
michael@0 | 430 | this._clearCache(); |
michael@0 | 431 | Services.obs.notifyObservers(null, "sessionstore-windows-restored", ""); |
michael@0 | 432 | } |
michael@0 | 433 | } |
michael@0 | 434 | |
michael@0 | 435 | // Add tab change listeners to all already existing tabs |
michael@0 | 436 | let tabs = aWindow.Browser.tabs; |
michael@0 | 437 | for (let i = 0; i < tabs.length; i++) |
michael@0 | 438 | this.onTabAdd(aWindow, tabs[i].browser, true); |
michael@0 | 439 | |
michael@0 | 440 | // Notification of tab add/remove/selection |
michael@0 | 441 | let tabContainer = aWindow.document.getElementById("tabs"); |
michael@0 | 442 | tabContainer.addEventListener("TabOpen", this, true); |
michael@0 | 443 | tabContainer.addEventListener("TabClose", this, true); |
michael@0 | 444 | tabContainer.addEventListener("TabRemove", this, true); |
michael@0 | 445 | tabContainer.addEventListener("TabSelect", this, true); |
michael@0 | 446 | }, |
michael@0 | 447 | |
michael@0 | 448 | onWindowClose: function ss_onWindowClose(aWindow) { |
michael@0 | 449 | // Ignore windows not tracked by SessionStore |
michael@0 | 450 | if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) |
michael@0 | 451 | return; |
michael@0 | 452 | |
michael@0 | 453 | let tabContainer = aWindow.document.getElementById("tabs"); |
michael@0 | 454 | tabContainer.removeEventListener("TabOpen", this, true); |
michael@0 | 455 | tabContainer.removeEventListener("TabClose", this, true); |
michael@0 | 456 | tabContainer.removeEventListener("TabRemove", this, true); |
michael@0 | 457 | tabContainer.removeEventListener("TabSelect", this, true); |
michael@0 | 458 | |
michael@0 | 459 | if (this._loadState == STATE_RUNNING) { |
michael@0 | 460 | // Update all window data for a last time |
michael@0 | 461 | this._collectWindowData(aWindow); |
michael@0 | 462 | |
michael@0 | 463 | // Clear this window from the list |
michael@0 | 464 | delete this._windows[aWindow.__SSID]; |
michael@0 | 465 | |
michael@0 | 466 | // Save the state without this window to disk |
michael@0 | 467 | this.saveStateDelayed(); |
michael@0 | 468 | } |
michael@0 | 469 | |
michael@0 | 470 | let tabs = aWindow.Browser.tabs; |
michael@0 | 471 | for (let i = 0; i < tabs.length; i++) |
michael@0 | 472 | this.onTabRemove(aWindow, tabs[i].browser, true); |
michael@0 | 473 | |
michael@0 | 474 | delete aWindow.__SSID; |
michael@0 | 475 | }, |
michael@0 | 476 | |
michael@0 | 477 | onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) { |
michael@0 | 478 | aBrowser.messageManager.addMessageListener("pageshow", this); |
michael@0 | 479 | aBrowser.messageManager.addMessageListener("Content:SessionHistory", this); |
michael@0 | 480 | aBrowser.messageManager.addMessageListener("SessionStore:collectFormdata", this); |
michael@0 | 481 | aBrowser.messageManager.addMessageListener("SessionStore:collectScrollPosition", this); |
michael@0 | 482 | |
michael@0 | 483 | if (!aNoNotification) |
michael@0 | 484 | this.saveStateDelayed(); |
michael@0 | 485 | this._updateCrashReportURL(aWindow); |
michael@0 | 486 | }, |
michael@0 | 487 | |
michael@0 | 488 | onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) { |
michael@0 | 489 | aBrowser.messageManager.removeMessageListener("pageshow", this); |
michael@0 | 490 | aBrowser.messageManager.removeMessageListener("Content:SessionHistory", this); |
michael@0 | 491 | aBrowser.messageManager.removeMessageListener("SessionStore:collectFormdata", this); |
michael@0 | 492 | aBrowser.messageManager.removeMessageListener("SessionStore:collectScrollPosition", this); |
michael@0 | 493 | |
michael@0 | 494 | // If this browser is being restored, skip any session save activity |
michael@0 | 495 | if (aBrowser.__SS_restore) |
michael@0 | 496 | return; |
michael@0 | 497 | |
michael@0 | 498 | delete aBrowser.__SS_data; |
michael@0 | 499 | |
michael@0 | 500 | if (!aNoNotification) |
michael@0 | 501 | this.saveStateDelayed(); |
michael@0 | 502 | }, |
michael@0 | 503 | |
michael@0 | 504 | onTabClose: function ss_onTabClose(aWindow, aBrowser) { |
michael@0 | 505 | if (this._maxTabsUndo == 0) |
michael@0 | 506 | return; |
michael@0 | 507 | |
michael@0 | 508 | if (aWindow.Browser.tabs.length > 0) { |
michael@0 | 509 | // Bundle this browser's data and extra data and save in the closedTabs |
michael@0 | 510 | // window property |
michael@0 | 511 | // |
michael@0 | 512 | // NB: The access to aBrowser.__SS_extdata throws during automation (in |
michael@0 | 513 | // browser_msgmgr_01). See bug 888736. |
michael@0 | 514 | let data = aBrowser.__SS_data; |
michael@0 | 515 | if (!data) { |
michael@0 | 516 | return; // Cannot restore an empty tab. |
michael@0 | 517 | } |
michael@0 | 518 | try { data.extData = aBrowser.__SS_extdata; } catch (e) { } |
michael@0 | 519 | |
michael@0 | 520 | this._windows[aWindow.__SSID]._closedTabs.unshift({ state: data }); |
michael@0 | 521 | let length = this._windows[aWindow.__SSID]._closedTabs.length; |
michael@0 | 522 | if (length > this._maxTabsUndo) |
michael@0 | 523 | this._windows[aWindow.__SSID]._closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo); |
michael@0 | 524 | } |
michael@0 | 525 | }, |
michael@0 | 526 | |
michael@0 | 527 | onTabLoad: function ss_onTabLoad(aWindow, aBrowser, aMessage) { |
michael@0 | 528 | // If this browser is being restored, skip any session save activity |
michael@0 | 529 | if (aBrowser.__SS_restore) |
michael@0 | 530 | return; |
michael@0 | 531 | |
michael@0 | 532 | // Ignore a transient "about:blank" |
michael@0 | 533 | if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") |
michael@0 | 534 | return; |
michael@0 | 535 | |
michael@0 | 536 | if (aMessage.name == "Content:SessionHistory") { |
michael@0 | 537 | delete aBrowser.__SS_data; |
michael@0 | 538 | this._collectTabData(aBrowser, aMessage.json); |
michael@0 | 539 | } |
michael@0 | 540 | |
michael@0 | 541 | // Save out the state as quickly as possible |
michael@0 | 542 | if (aMessage.name == "pageshow") |
michael@0 | 543 | this.saveStateNow(); |
michael@0 | 544 | |
michael@0 | 545 | this._updateCrashReportURL(aWindow); |
michael@0 | 546 | }, |
michael@0 | 547 | |
michael@0 | 548 | onTabSelect: function ss_onTabSelect(aWindow, aBrowser) { |
michael@0 | 549 | if (this._loadState != STATE_RUNNING) |
michael@0 | 550 | return; |
michael@0 | 551 | |
michael@0 | 552 | let index = aWindow.Elements.browsers.selectedIndex; |
michael@0 | 553 | this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based |
michael@0 | 554 | |
michael@0 | 555 | // Restore the resurrected browser |
michael@0 | 556 | if (aBrowser.__SS_restore) { |
michael@0 | 557 | let data = aBrowser.__SS_data; |
michael@0 | 558 | if (data.entries.length > 0) { |
michael@0 | 559 | let json = { |
michael@0 | 560 | uri: data.entries[data.index - 1].url, |
michael@0 | 561 | flags: null, |
michael@0 | 562 | entries: data.entries, |
michael@0 | 563 | index: data.index |
michael@0 | 564 | }; |
michael@0 | 565 | aBrowser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json); |
michael@0 | 566 | } |
michael@0 | 567 | |
michael@0 | 568 | delete aBrowser.__SS_restore; |
michael@0 | 569 | } |
michael@0 | 570 | |
michael@0 | 571 | this._updateCrashReportURL(aWindow); |
michael@0 | 572 | }, |
michael@0 | 573 | |
michael@0 | 574 | saveStateDelayed: function ss_saveStateDelayed() { |
michael@0 | 575 | if (!this._saveTimer) { |
michael@0 | 576 | // Interval until the next disk operation is allowed |
michael@0 | 577 | let minimalDelay = this._lastSaveTime + this._interval - Date.now(); |
michael@0 | 578 | |
michael@0 | 579 | // If we have to wait, set a timer, otherwise saveState directly |
michael@0 | 580 | let delay = Math.max(minimalDelay, 2000); |
michael@0 | 581 | if (delay > 0) { |
michael@0 | 582 | this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); |
michael@0 | 583 | this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); |
michael@0 | 584 | } else { |
michael@0 | 585 | this.saveState(); |
michael@0 | 586 | } |
michael@0 | 587 | } |
michael@0 | 588 | }, |
michael@0 | 589 | |
michael@0 | 590 | saveStateNow: function ss_saveStateNow() { |
michael@0 | 591 | // Kill any queued timer and save immediately |
michael@0 | 592 | if (this._saveTimer) { |
michael@0 | 593 | this._saveTimer.cancel(); |
michael@0 | 594 | this._saveTimer = null; |
michael@0 | 595 | } |
michael@0 | 596 | this.saveState(); |
michael@0 | 597 | }, |
michael@0 | 598 | |
michael@0 | 599 | saveState: function ss_saveState() { |
michael@0 | 600 | let data = this._getCurrentState(); |
michael@0 | 601 | // sanity check before we overwrite the session file |
michael@0 | 602 | if (data.windows && data.windows.length && data.selectedWindow) { |
michael@0 | 603 | this._writeFile(this._sessionFile, JSON.stringify(data)); |
michael@0 | 604 | |
michael@0 | 605 | this._lastSaveTime = Date.now(); |
michael@0 | 606 | } else { |
michael@0 | 607 | dump("SessionStore: Not saving state with invalid data: " + JSON.stringify(data) + "\n"); |
michael@0 | 608 | } |
michael@0 | 609 | }, |
michael@0 | 610 | |
michael@0 | 611 | _getCurrentState: function ss_getCurrentState() { |
michael@0 | 612 | let self = this; |
michael@0 | 613 | this._forEachBrowserWindow(function(aWindow) { |
michael@0 | 614 | self._collectWindowData(aWindow); |
michael@0 | 615 | }); |
michael@0 | 616 | |
michael@0 | 617 | let data = { windows: [] }; |
michael@0 | 618 | for (let i = 0; i < this._orderedWindows.length; i++) |
michael@0 | 619 | data.windows.push(this._windows[this._orderedWindows[i]]); |
michael@0 | 620 | data.selectedWindow = this._selectedWindow; |
michael@0 | 621 | return data; |
michael@0 | 622 | }, |
michael@0 | 623 | |
michael@0 | 624 | _collectTabData: function ss__collectTabData(aBrowser, aHistory) { |
michael@0 | 625 | // If this browser is being restored, skip any session save activity |
michael@0 | 626 | if (aBrowser.__SS_restore) |
michael@0 | 627 | return; |
michael@0 | 628 | |
michael@0 | 629 | let aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 }; |
michael@0 | 630 | |
michael@0 | 631 | let tabData = {}; |
michael@0 | 632 | tabData.entries = aHistory.entries; |
michael@0 | 633 | tabData.index = aHistory.index; |
michael@0 | 634 | tabData.attributes = { image: aBrowser.mIconURL }; |
michael@0 | 635 | |
michael@0 | 636 | aBrowser.__SS_data = tabData; |
michael@0 | 637 | }, |
michael@0 | 638 | |
michael@0 | 639 | _getTabData: function(aWindow) { |
michael@0 | 640 | return aWindow.Browser.tabs |
michael@0 | 641 | .filter(tab => !tab.isPrivate && tab.browser.__SS_data) |
michael@0 | 642 | .map(tab => { |
michael@0 | 643 | let browser = tab.browser; |
michael@0 | 644 | let tabData = browser.__SS_data; |
michael@0 | 645 | if (browser.__SS_extdata) |
michael@0 | 646 | tabData.extData = browser.__SS_extdata; |
michael@0 | 647 | return tabData; |
michael@0 | 648 | }); |
michael@0 | 649 | }, |
michael@0 | 650 | |
michael@0 | 651 | _collectWindowData: function ss__collectWindowData(aWindow) { |
michael@0 | 652 | // Ignore windows not tracked by SessionStore |
michael@0 | 653 | if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) |
michael@0 | 654 | return; |
michael@0 | 655 | |
michael@0 | 656 | let winData = this._windows[aWindow.__SSID]; |
michael@0 | 657 | |
michael@0 | 658 | let index = aWindow.Elements.browsers.selectedIndex; |
michael@0 | 659 | winData.selected = parseInt(index) + 1; // 1-based |
michael@0 | 660 | |
michael@0 | 661 | let tabData = this._getTabData(aWindow); |
michael@0 | 662 | winData.tabs = tabData.concat(this._tabsFromOtherGroups); |
michael@0 | 663 | }, |
michael@0 | 664 | |
michael@0 | 665 | _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) { |
michael@0 | 666 | let windowsEnum = Services.wm.getEnumerator("navigator:browser"); |
michael@0 | 667 | while (windowsEnum.hasMoreElements()) { |
michael@0 | 668 | let window = windowsEnum.getNext(); |
michael@0 | 669 | if (window.__SSID && !window.closed) |
michael@0 | 670 | aFunc.call(this, window); |
michael@0 | 671 | } |
michael@0 | 672 | }, |
michael@0 | 673 | |
michael@0 | 674 | _writeFile: function ss_writeFile(aFile, aData) { |
michael@0 | 675 | let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); |
michael@0 | 676 | stateString.data = aData; |
michael@0 | 677 | Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); |
michael@0 | 678 | |
michael@0 | 679 | // Don't touch the file if an observer has deleted all state data |
michael@0 | 680 | if (!stateString.data) |
michael@0 | 681 | return; |
michael@0 | 682 | |
michael@0 | 683 | // Initialize the file output stream. |
michael@0 | 684 | let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); |
michael@0 | 685 | ostream.init(aFile, 0x02 | 0x08 | 0x20, 0600, ostream.DEFER_OPEN); |
michael@0 | 686 | |
michael@0 | 687 | // Obtain a converter to convert our data to a UTF-8 encoded input stream. |
michael@0 | 688 | let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); |
michael@0 | 689 | converter.charset = "UTF-8"; |
michael@0 | 690 | |
michael@0 | 691 | // Asynchronously copy the data to the file. |
michael@0 | 692 | let istream = converter.convertToInputStream(aData); |
michael@0 | 693 | NetUtil.asyncCopy(istream, ostream, function(rc) { |
michael@0 | 694 | if (Components.isSuccessCode(rc)) { |
michael@0 | 695 | if (Services.startup.shuttingDown) { |
michael@0 | 696 | Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", ""); |
michael@0 | 697 | } |
michael@0 | 698 | Services.obs.notifyObservers(null, "sessionstore-state-write-complete", ""); |
michael@0 | 699 | } |
michael@0 | 700 | }); |
michael@0 | 701 | }, |
michael@0 | 702 | |
michael@0 | 703 | _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) { |
michael@0 | 704 | #ifdef MOZ_CRASHREPORTER |
michael@0 | 705 | try { |
michael@0 | 706 | let currentURI = aWindow.Browser.selectedBrowser.currentURI.clone(); |
michael@0 | 707 | // if the current URI contains a username/password, remove it |
michael@0 | 708 | try { |
michael@0 | 709 | currentURI.userPass = ""; |
michael@0 | 710 | } |
michael@0 | 711 | catch (ex) { } // ignore failures on about: URIs |
michael@0 | 712 | |
michael@0 | 713 | CrashReporter.annotateCrashReport("URL", currentURI.spec); |
michael@0 | 714 | } |
michael@0 | 715 | catch (ex) { |
michael@0 | 716 | // don't make noise when crashreporter is built but not enabled |
michael@0 | 717 | if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) |
michael@0 | 718 | Components.utils.reportError("SessionStore:" + ex); |
michael@0 | 719 | } |
michael@0 | 720 | #endif |
michael@0 | 721 | }, |
michael@0 | 722 | |
michael@0 | 723 | getBrowserState: function ss_getBrowserState() { |
michael@0 | 724 | let data = this._getCurrentState(); |
michael@0 | 725 | return JSON.stringify(data); |
michael@0 | 726 | }, |
michael@0 | 727 | |
michael@0 | 728 | getClosedTabCount: function ss_getClosedTabCount(aWindow) { |
michael@0 | 729 | if (!aWindow || !aWindow.__SSID) |
michael@0 | 730 | return 0; // not a browser window, or not otherwise tracked by SS. |
michael@0 | 731 | |
michael@0 | 732 | return this._windows[aWindow.__SSID]._closedTabs.length; |
michael@0 | 733 | }, |
michael@0 | 734 | |
michael@0 | 735 | getClosedTabData: function ss_getClosedTabData(aWindow) { |
michael@0 | 736 | if (!aWindow.__SSID) |
michael@0 | 737 | throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
michael@0 | 738 | |
michael@0 | 739 | return JSON.stringify(this._windows[aWindow.__SSID]._closedTabs); |
michael@0 | 740 | }, |
michael@0 | 741 | |
michael@0 | 742 | undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { |
michael@0 | 743 | if (!aWindow.__SSID) |
michael@0 | 744 | throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
michael@0 | 745 | |
michael@0 | 746 | let closedTabs = this._windows[aWindow.__SSID]._closedTabs; |
michael@0 | 747 | if (!closedTabs) |
michael@0 | 748 | return null; |
michael@0 | 749 | |
michael@0 | 750 | // default to the most-recently closed tab |
michael@0 | 751 | aIndex = aIndex || 0; |
michael@0 | 752 | if (!(aIndex in closedTabs)) |
michael@0 | 753 | throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
michael@0 | 754 | |
michael@0 | 755 | // fetch the data of closed tab, while removing it from the array |
michael@0 | 756 | let closedTab = closedTabs.splice(aIndex, 1).shift(); |
michael@0 | 757 | |
michael@0 | 758 | // create a new tab and bring to front |
michael@0 | 759 | let tab = aWindow.Browser.addTab(closedTab.state.entries[closedTab.state.index - 1].url, true); |
michael@0 | 760 | |
michael@0 | 761 | tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", { |
michael@0 | 762 | uri: closedTab.state.entries[closedTab.state.index - 1].url, |
michael@0 | 763 | flags: null, |
michael@0 | 764 | entries: closedTab.state.entries, |
michael@0 | 765 | index: closedTab.state.index |
michael@0 | 766 | }); |
michael@0 | 767 | |
michael@0 | 768 | // Put back the extra data |
michael@0 | 769 | tab.browser.__SS_extdata = closedTab.extData; |
michael@0 | 770 | |
michael@0 | 771 | return tab.chromeTab; |
michael@0 | 772 | }, |
michael@0 | 773 | |
michael@0 | 774 | forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) { |
michael@0 | 775 | if (!aWindow.__SSID) |
michael@0 | 776 | throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
michael@0 | 777 | |
michael@0 | 778 | let closedTabs = this._windows[aWindow.__SSID]._closedTabs; |
michael@0 | 779 | |
michael@0 | 780 | // default to the most-recently closed tab |
michael@0 | 781 | aIndex = aIndex || 0; |
michael@0 | 782 | if (!(aIndex in closedTabs)) |
michael@0 | 783 | throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
michael@0 | 784 | |
michael@0 | 785 | // remove closed tab from the array |
michael@0 | 786 | closedTabs.splice(aIndex, 1); |
michael@0 | 787 | }, |
michael@0 | 788 | |
michael@0 | 789 | getTabValue: function ss_getTabValue(aTab, aKey) { |
michael@0 | 790 | let browser = aTab.linkedBrowser; |
michael@0 | 791 | let data = browser.__SS_extdata || {}; |
michael@0 | 792 | return data[aKey] || ""; |
michael@0 | 793 | }, |
michael@0 | 794 | |
michael@0 | 795 | setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { |
michael@0 | 796 | let browser = aTab.linkedBrowser; |
michael@0 | 797 | |
michael@0 | 798 | // Thumbnails are actually stored in the cache, so do the save and update the URI |
michael@0 | 799 | if (aKey == "thumbnail") { |
michael@0 | 800 | let file = this._sessionCache.clone(); |
michael@0 | 801 | file.append("thumbnail-" + browser.contentWindowId); |
michael@0 | 802 | file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); |
michael@0 | 803 | |
michael@0 | 804 | let source = Services.io.newURI(aStringValue, "UTF8", null); |
michael@0 | 805 | let target = Services.io.newFileURI(file) |
michael@0 | 806 | |
michael@0 | 807 | let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist); |
michael@0 | 808 | persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; |
michael@0 | 809 | persist.saveURI(source, null, null, null, null, file); |
michael@0 | 810 | |
michael@0 | 811 | aStringValue = target.spec; |
michael@0 | 812 | } |
michael@0 | 813 | |
michael@0 | 814 | if (!browser.__SS_extdata) |
michael@0 | 815 | browser.__SS_extdata = {}; |
michael@0 | 816 | browser.__SS_extdata[aKey] = aStringValue; |
michael@0 | 817 | this.saveStateDelayed(); |
michael@0 | 818 | }, |
michael@0 | 819 | |
michael@0 | 820 | deleteTabValue: function ss_deleteTabValue(aTab, aKey) { |
michael@0 | 821 | let browser = aTab.linkedBrowser; |
michael@0 | 822 | if (browser.__SS_extdata && browser.__SS_extdata[aKey]) |
michael@0 | 823 | delete browser.__SS_extdata[aKey]; |
michael@0 | 824 | else |
michael@0 | 825 | throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); |
michael@0 | 826 | }, |
michael@0 | 827 | |
michael@0 | 828 | shouldRestore: function ss_shouldRestore() { |
michael@0 | 829 | return this._shouldRestore || (3 == Services.prefs.getIntPref("browser.startup.page")); |
michael@0 | 830 | }, |
michael@0 | 831 | |
michael@0 | 832 | restoreLastSession: function ss_restoreLastSession(aBringToFront) { |
michael@0 | 833 | let self = this; |
michael@0 | 834 | function notifyObservers(aMessage) { |
michael@0 | 835 | self._clearCache(); |
michael@0 | 836 | Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || ""); |
michael@0 | 837 | } |
michael@0 | 838 | |
michael@0 | 839 | // The previous session data has already been renamed to the backup file |
michael@0 | 840 | if (!this._sessionFileBackup.exists()) { |
michael@0 | 841 | notifyObservers("fail") |
michael@0 | 842 | return; |
michael@0 | 843 | } |
michael@0 | 844 | |
michael@0 | 845 | try { |
michael@0 | 846 | let channel = NetUtil.newChannel(this._sessionFileBackup); |
michael@0 | 847 | channel.contentType = "application/json"; |
michael@0 | 848 | NetUtil.asyncFetch(channel, function(aStream, aResult) { |
michael@0 | 849 | if (!Components.isSuccessCode(aResult)) { |
michael@0 | 850 | Cu.reportError("SessionStore: Could not read from sessionstore.bak file"); |
michael@0 | 851 | notifyObservers("fail"); |
michael@0 | 852 | return; |
michael@0 | 853 | } |
michael@0 | 854 | |
michael@0 | 855 | // Read session state file into a string and let observers modify the state before it's being used |
michael@0 | 856 | let state = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); |
michael@0 | 857 | state.data = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || ""; |
michael@0 | 858 | aStream.close(); |
michael@0 | 859 | |
michael@0 | 860 | Services.obs.notifyObservers(state, "sessionstore-state-read", ""); |
michael@0 | 861 | |
michael@0 | 862 | let data = null; |
michael@0 | 863 | try { |
michael@0 | 864 | data = JSON.parse(state.data); |
michael@0 | 865 | } catch (ex) { |
michael@0 | 866 | Cu.reportError("SessionStore: Could not parse JSON: " + ex); |
michael@0 | 867 | } |
michael@0 | 868 | |
michael@0 | 869 | if (!data || data.windows.length == 0) { |
michael@0 | 870 | notifyObservers("fail"); |
michael@0 | 871 | return; |
michael@0 | 872 | } |
michael@0 | 873 | |
michael@0 | 874 | let window = Services.wm.getMostRecentWindow("navigator:browser"); |
michael@0 | 875 | |
michael@0 | 876 | if (typeof data.selectedWindow == "number") { |
michael@0 | 877 | this._selectedWindow = data.selectedWindow; |
michael@0 | 878 | } |
michael@0 | 879 | let windowIndex = this._selectedWindow - 1; |
michael@0 | 880 | let tabs = data.windows[windowIndex].tabs; |
michael@0 | 881 | let selected = data.windows[windowIndex].selected; |
michael@0 | 882 | |
michael@0 | 883 | let currentGroupId; |
michael@0 | 884 | try { |
michael@0 | 885 | currentGroupId = JSON.parse(data.windows[windowIndex].extData["tabview-groups"]).activeGroupId; |
michael@0 | 886 | } catch (ex) { /* currentGroupId is undefined if user has no tab groups */ } |
michael@0 | 887 | |
michael@0 | 888 | // Move all window data from sessionstore.js to this._windows. |
michael@0 | 889 | this._orderedWindows = []; |
michael@0 | 890 | for (let i = 0; i < data.windows.length; i++) { |
michael@0 | 891 | let SSID; |
michael@0 | 892 | if (i != windowIndex) { |
michael@0 | 893 | SSID = "window" + gUUIDGenerator.generateUUID().toString(); |
michael@0 | 894 | this._windows[SSID] = data.windows[i]; |
michael@0 | 895 | } else { |
michael@0 | 896 | SSID = window.__SSID; |
michael@0 | 897 | this._windows[SSID].extData = data.windows[i].extData; |
michael@0 | 898 | this._windows[SSID]._closedTabs = |
michael@0 | 899 | this._windows[SSID]._closedTabs.concat(data.windows[i]._closedTabs); |
michael@0 | 900 | } |
michael@0 | 901 | this._orderedWindows.push(SSID); |
michael@0 | 902 | } |
michael@0 | 903 | |
michael@0 | 904 | if (selected > tabs.length) // Clamp the selected index if it's bogus |
michael@0 | 905 | selected = 1; |
michael@0 | 906 | |
michael@0 | 907 | for (let i=0; i<tabs.length; i++) { |
michael@0 | 908 | let tabData = tabs[i]; |
michael@0 | 909 | let tabGroupId = (typeof currentGroupId == "number") ? |
michael@0 | 910 | JSON.parse(tabData.extData["tabview-tab"]).groupID : null; |
michael@0 | 911 | |
michael@0 | 912 | if (tabGroupId && tabGroupId != currentGroupId) { |
michael@0 | 913 | this._tabsFromOtherGroups.push(tabData); |
michael@0 | 914 | continue; |
michael@0 | 915 | } |
michael@0 | 916 | |
michael@0 | 917 | // We must have selected tabs as soon as possible, so we let all tabs be selected |
michael@0 | 918 | // until we get the real selected tab. Then we stop selecting tabs. The end result |
michael@0 | 919 | // is that the right tab is selected, but we also don't get a bunch of errors |
michael@0 | 920 | let bringToFront = (i + 1 <= selected) && aBringToFront; |
michael@0 | 921 | let tab = window.Browser.addTab(tabData.entries[tabData.index - 1].url, bringToFront); |
michael@0 | 922 | |
michael@0 | 923 | // Start a real load for the selected tab |
michael@0 | 924 | if (i + 1 == selected) { |
michael@0 | 925 | let json = { |
michael@0 | 926 | uri: tabData.entries[tabData.index - 1].url, |
michael@0 | 927 | flags: null, |
michael@0 | 928 | entries: tabData.entries, |
michael@0 | 929 | index: tabData.index |
michael@0 | 930 | }; |
michael@0 | 931 | tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json); |
michael@0 | 932 | } else { |
michael@0 | 933 | // Make sure the browser has its session data for the delay reload |
michael@0 | 934 | tab.browser.__SS_data = tabData; |
michael@0 | 935 | tab.browser.__SS_restore = true; |
michael@0 | 936 | |
michael@0 | 937 | // Restore current title |
michael@0 | 938 | tab.chromeTab.updateTitle(tabData.entries[tabData.index - 1].title); |
michael@0 | 939 | } |
michael@0 | 940 | |
michael@0 | 941 | tab.browser.__SS_tabFormData = tabData |
michael@0 | 942 | tab.browser.__SS_extdata = tabData.extData; |
michael@0 | 943 | } |
michael@0 | 944 | |
michael@0 | 945 | notifyObservers(); |
michael@0 | 946 | }.bind(this)); |
michael@0 | 947 | } catch (ex) { |
michael@0 | 948 | Cu.reportError("SessionStore: Could not read from sessionstore.bak file: " + ex); |
michael@0 | 949 | notifyObservers("fail"); |
michael@0 | 950 | } |
michael@0 | 951 | } |
michael@0 | 952 | }; |
michael@0 | 953 | |
michael@0 | 954 | this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]); |