browser/metro/components/SessionStore.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

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]);

mercurial