1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/components/SessionStore.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,954 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const Cc = Components.classes; 1.9 +const Ci = Components.interfaces; 1.10 +const Cu = Components.utils; 1.11 +const Cr = Components.results; 1.12 + 1.13 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.14 +Cu.import("resource://gre/modules/Services.jsm"); 1.15 +Cu.import("resource://gre/modules/WindowsPrefSync.jsm"); 1.16 + 1.17 +#ifdef MOZ_CRASHREPORTER 1.18 +XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", 1.19 + "@mozilla.org/xre/app-info;1", "nsICrashReporter"); 1.20 +#endif 1.21 + 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor", 1.23 + "resource://gre/modules/CrashMonitor.jsm"); 1.24 + 1.25 +XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator", 1.26 + "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); 1.27 + 1.28 +XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { 1.29 + Cu.import("resource://gre/modules/NetUtil.jsm"); 1.30 + return NetUtil; 1.31 +}); 1.32 + 1.33 +XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", 1.34 + "resource://gre/modules/UITelemetry.jsm"); 1.35 + 1.36 +// ----------------------------------------------------------------------- 1.37 +// Session Store 1.38 +// ----------------------------------------------------------------------- 1.39 + 1.40 +const STATE_STOPPED = 0; 1.41 +const STATE_RUNNING = 1; 1.42 +const STATE_QUITTING = -1; 1.43 + 1.44 +function SessionStore() { } 1.45 + 1.46 +SessionStore.prototype = { 1.47 + classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"), 1.48 + 1.49 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, 1.50 + Ci.nsIDOMEventListener, 1.51 + Ci.nsIObserver, 1.52 + Ci.nsISupportsWeakReference]), 1.53 + 1.54 + _windows: {}, 1.55 + _tabsFromOtherGroups: [], 1.56 + _selectedWindow: 1, 1.57 + _orderedWindows: [], 1.58 + _lastSaveTime: 0, 1.59 + _lastSessionTime: 0, 1.60 + _interval: 10000, 1.61 + _maxTabsUndo: 1, 1.62 + _shouldRestore: false, 1.63 + 1.64 + // Tab telemetry variables 1.65 + _maxTabsOpen: 1, 1.66 + 1.67 + init: function ss_init() { 1.68 + // Get file references 1.69 + this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); 1.70 + this._sessionFileBackup = this._sessionFile.clone(); 1.71 + this._sessionCache = this._sessionFile.clone(); 1.72 + this._sessionFile.append("sessionstore.js"); 1.73 + this._sessionFileBackup.append("sessionstore.bak"); 1.74 + this._sessionCache.append("sessionstoreCache"); 1.75 + 1.76 + this._loadState = STATE_STOPPED; 1.77 + 1.78 + try { 1.79 + UITelemetry.addSimpleMeasureFunction("metro-tabs", 1.80 + this._getTabStats.bind(this)); 1.81 + } catch (ex) { 1.82 + // swallow exception that occurs if metro-tabs measure is already set up 1.83 + } 1.84 + 1.85 + CrashMonitor.previousCheckpoints.then(checkpoints => { 1.86 + let previousSessionCrashed = false; 1.87 + 1.88 + if (checkpoints) { 1.89 + // If the previous session finished writing the final state, we'll 1.90 + // assume there was no crash. 1.91 + previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"]; 1.92 + } else { 1.93 + // If no checkpoints are saved, this is the first run with CrashMonitor or the 1.94 + // metroSessionCheckpoints file was corrupted/deleted, so fallback to defining 1.95 + // a crash as init-ing with an unexpected previousExecutionState 1.96 + // 1 == RUNNING, 2 == SUSPENDED 1.97 + previousSessionCrashed = Services.metro.previousExecutionState == 1 || 1.98 + Services.metro.previousExecutionState == 2; 1.99 + } 1.100 + 1.101 + Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!previousSessionCrashed); 1.102 + }); 1.103 + 1.104 + try { 1.105 + let shutdownWasUnclean = false; 1.106 + 1.107 + if (this._sessionFileBackup.exists()) { 1.108 + this._sessionFileBackup.remove(false); 1.109 + shutdownWasUnclean = true; 1.110 + } 1.111 + 1.112 + if (this._sessionFile.exists()) { 1.113 + this._sessionFile.copyTo(null, this._sessionFileBackup.leafName); 1.114 + 1.115 + switch(Services.metro.previousExecutionState) { 1.116 + // 0 == NotRunning 1.117 + case 0: 1.118 + // Disable crash recovery if we have exceeded the timeout 1.119 + this._lastSessionTime = this._sessionFile.lastModifiedTime; 1.120 + let delta = Date.now() - this._lastSessionTime; 1.121 + let timeout = 1.122 + Services.prefs.getIntPref( 1.123 + "browser.sessionstore.resume_from_crash_timeout"); 1.124 + this._shouldRestore = shutdownWasUnclean 1.125 + && (delta < (timeout * 60000)); 1.126 + break; 1.127 + // 1 == Running 1.128 + case 1: 1.129 + // We should never encounter this situation 1.130 + Components.utils.reportError("SessionRestore.init called with " 1.131 + + "previous execution state 'Running'"); 1.132 + this._shouldRestore = true; 1.133 + break; 1.134 + // 2 == Suspended 1.135 + case 2: 1.136 + // We should never encounter this situation 1.137 + Components.utils.reportError("SessionRestore.init called with " 1.138 + + "previous execution state 'Suspended'"); 1.139 + this._shouldRestore = true; 1.140 + break; 1.141 + // 3 == Terminated 1.142 + case 3: 1.143 + // Terminated means that Windows terminated our already-suspended 1.144 + // process to get back some resources. When we re-launch, we want 1.145 + // to provide the illusion that our process was suspended the 1.146 + // whole time, and never terminated. 1.147 + this._shouldRestore = true; 1.148 + break; 1.149 + // 4 == ClosedByUser 1.150 + case 4: 1.151 + // ClosedByUser indicates that the user performed a "close" gesture 1.152 + // on our tile. We should act as if the browser closed normally, 1.153 + // even if we were closed from a suspended state (in which case 1.154 + // we'll have determined that it was an unclean shtudown) 1.155 + this._shouldRestore = false; 1.156 + break; 1.157 + } 1.158 + } 1.159 + 1.160 + if (!this._sessionCache.exists() || !this._sessionCache.isDirectory()) { 1.161 + this._sessionCache.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); 1.162 + } 1.163 + } catch (ex) { 1.164 + Cu.reportError(ex); // file was write-locked? 1.165 + } 1.166 + 1.167 + this._interval = Services.prefs.getIntPref("browser.sessionstore.interval"); 1.168 + this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo"); 1.169 + 1.170 + // Disable crash recovery if it has been turned off 1.171 + if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash")) 1.172 + this._shouldRestore = false; 1.173 + 1.174 + // Do we need to restore session just this once, in case of a restart? 1.175 + if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) { 1.176 + Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false); 1.177 + this._shouldRestore = true; 1.178 + } 1.179 + }, 1.180 + 1.181 + _clearDisk: function ss_clearDisk() { 1.182 + if (this._sessionFile.exists()) { 1.183 + try { 1.184 + this._sessionFile.remove(false); 1.185 + } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? 1.186 + } 1.187 + if (this._sessionFileBackup.exists()) { 1.188 + try { 1.189 + this._sessionFileBackup.remove(false); 1.190 + } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now? 1.191 + } 1.192 + 1.193 + this._clearCache(); 1.194 + }, 1.195 + 1.196 + _clearCache: function ss_clearCache() { 1.197 + // First, let's get a list of files we think should be active 1.198 + let activeFiles = []; 1.199 + this._forEachBrowserWindow(function(aWindow) { 1.200 + let tabs = aWindow.Browser.tabs; 1.201 + for (let i = 0; i < tabs.length; i++) { 1.202 + let browser = tabs[i].browser; 1.203 + if (browser.__SS_extdata && "thumbnail" in browser.__SS_extdata) 1.204 + activeFiles.push(browser.__SS_extdata.thumbnail); 1.205 + } 1.206 + }); 1.207 + 1.208 + // Now, let's find the stale files in the cache folder 1.209 + let staleFiles = []; 1.210 + let cacheFiles = this._sessionCache.directoryEntries; 1.211 + while (cacheFiles.hasMoreElements()) { 1.212 + let file = cacheFiles.getNext().QueryInterface(Ci.nsILocalFile); 1.213 + let fileURI = Services.io.newFileURI(file); 1.214 + if (activeFiles.indexOf(fileURI) == -1) 1.215 + staleFiles.push(file); 1.216 + } 1.217 + 1.218 + // Remove the stale files in a separate step to keep the enumerator from 1.219 + // messing up if we remove the files as we collect them. 1.220 + staleFiles.forEach(function(aFile) { 1.221 + aFile.remove(false); 1.222 + }) 1.223 + }, 1.224 + 1.225 + _getTabStats: function() { 1.226 + return { 1.227 + currTabCount: this._currTabCount, 1.228 + maxTabCount: this._maxTabsOpen 1.229 + }; 1.230 + }, 1.231 + 1.232 + observe: function ss_observe(aSubject, aTopic, aData) { 1.233 + let self = this; 1.234 + let observerService = Services.obs; 1.235 + switch (aTopic) { 1.236 + case "app-startup": 1.237 + observerService.addObserver(this, "final-ui-startup", true); 1.238 + observerService.addObserver(this, "domwindowopened", true); 1.239 + observerService.addObserver(this, "domwindowclosed", true); 1.240 + observerService.addObserver(this, "browser-lastwindow-close-granted", true); 1.241 + observerService.addObserver(this, "browser:purge-session-history", true); 1.242 + observerService.addObserver(this, "quit-application-requested", true); 1.243 + observerService.addObserver(this, "quit-application-granted", true); 1.244 + observerService.addObserver(this, "quit-application", true); 1.245 + observerService.addObserver(this, "reset-telemetry-vars", true); 1.246 + break; 1.247 + case "final-ui-startup": 1.248 + observerService.removeObserver(this, "final-ui-startup"); 1.249 + if (WindowsPrefSync) { 1.250 + // Pulls in Desktop controlled prefs and pushes out Metro controlled prefs 1.251 + WindowsPrefSync.init(); 1.252 + } 1.253 + this.init(); 1.254 + break; 1.255 + case "domwindowopened": 1.256 + let window = aSubject; 1.257 + window.addEventListener("load", function() { 1.258 + self.onWindowOpen(window); 1.259 + window.removeEventListener("load", arguments.callee, false); 1.260 + }, false); 1.261 + break; 1.262 + case "domwindowclosed": // catch closed windows 1.263 + this.onWindowClose(aSubject); 1.264 + break; 1.265 + case "browser-lastwindow-close-granted": 1.266 + // If a save has been queued, kill the timer and save state now 1.267 + if (this._saveTimer) { 1.268 + this._saveTimer.cancel(); 1.269 + this._saveTimer = null; 1.270 + this.saveState(); 1.271 + } 1.272 + 1.273 + // Freeze the data at what we've got (ignoring closing windows) 1.274 + this._loadState = STATE_QUITTING; 1.275 + break; 1.276 + case "quit-application-requested": 1.277 + // Get a current snapshot of all windows 1.278 + this._forEachBrowserWindow(function(aWindow) { 1.279 + self._collectWindowData(aWindow); 1.280 + }); 1.281 + break; 1.282 + case "quit-application-granted": 1.283 + // Get a current snapshot of all windows 1.284 + this._forEachBrowserWindow(function(aWindow) { 1.285 + self._collectWindowData(aWindow); 1.286 + }); 1.287 + 1.288 + // Freeze the data at what we've got (ignoring closing windows) 1.289 + this._loadState = STATE_QUITTING; 1.290 + break; 1.291 + case "quit-application": 1.292 + // If we are restarting, lets restore the tabs 1.293 + if (aData == "restart") { 1.294 + Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true); 1.295 + 1.296 + // Ignore purges when restarting. The notification is fired after "quit-application". 1.297 + Services.obs.removeObserver(this, "browser:purge-session-history"); 1.298 + } 1.299 + 1.300 + // Freeze the data at what we've got (ignoring closing windows) 1.301 + this._loadState = STATE_QUITTING; 1.302 + 1.303 + // No need for this back up, we are shutting down just fine 1.304 + if (this._sessionFileBackup.exists()) 1.305 + this._sessionFileBackup.remove(false); 1.306 + 1.307 + observerService.removeObserver(this, "domwindowopened"); 1.308 + observerService.removeObserver(this, "domwindowclosed"); 1.309 + observerService.removeObserver(this, "browser-lastwindow-close-granted"); 1.310 + observerService.removeObserver(this, "quit-application-requested"); 1.311 + observerService.removeObserver(this, "quit-application-granted"); 1.312 + observerService.removeObserver(this, "quit-application"); 1.313 + observerService.removeObserver(this, "reset-telemetry-vars"); 1.314 + 1.315 + // If a save has been queued, kill the timer and save state now 1.316 + if (this._saveTimer) { 1.317 + this._saveTimer.cancel(); 1.318 + this._saveTimer = null; 1.319 + } 1.320 + this.saveState(); 1.321 + break; 1.322 + case "browser:purge-session-history": // catch sanitization 1.323 + this._clearDisk(); 1.324 + 1.325 + // If the browser is shutting down, simply return after clearing the 1.326 + // session data on disk as this notification fires after the 1.327 + // quit-application notification so the browser is about to exit. 1.328 + if (this._loadState == STATE_QUITTING) 1.329 + return; 1.330 + 1.331 + // Clear all data about closed tabs 1.332 + for (let [ssid, win] in Iterator(this._windows)) 1.333 + win._closedTabs = []; 1.334 + 1.335 + if (this._loadState == STATE_RUNNING) { 1.336 + // Save the purged state immediately 1.337 + this.saveStateNow(); 1.338 + } 1.339 + break; 1.340 + case "timer-callback": 1.341 + // Timer call back for delayed saving 1.342 + this._saveTimer = null; 1.343 + this.saveState(); 1.344 + break; 1.345 + case "reset-telemetry-vars": 1.346 + // Used in mochitests only. 1.347 + this._maxTabsOpen = 1; 1.348 + } 1.349 + }, 1.350 + 1.351 + updateTabTelemetryVars: function(window) { 1.352 + this._currTabCount = window.Browser.tabs.length; 1.353 + if (this._currTabCount > this._maxTabsOpen) { 1.354 + this._maxTabsOpen = this._currTabCount; 1.355 + } 1.356 + }, 1.357 + 1.358 + handleEvent: function ss_handleEvent(aEvent) { 1.359 + let window = aEvent.currentTarget.ownerDocument.defaultView; 1.360 + switch (aEvent.type) { 1.361 + case "load": 1.362 + browser = aEvent.currentTarget; 1.363 + if (aEvent.target == browser.contentDocument && browser.__SS_tabFormData) { 1.364 + browser.messageManager.sendAsyncMessage("SessionStore:restoreSessionTabData", { 1.365 + formdata: browser.__SS_tabFormData.formdata, 1.366 + scroll: browser.__SS_tabFormData.scroll 1.367 + }); 1.368 + } 1.369 + break; 1.370 + case "TabOpen": 1.371 + this.updateTabTelemetryVars(window); 1.372 + let browser = aEvent.originalTarget.linkedBrowser; 1.373 + browser.addEventListener("load", this, true); 1.374 + case "TabClose": { 1.375 + let browser = aEvent.originalTarget.linkedBrowser; 1.376 + if (aEvent.type == "TabOpen") { 1.377 + this.onTabAdd(window, browser); 1.378 + } 1.379 + else { 1.380 + this.onTabClose(window, browser); 1.381 + this.onTabRemove(window, browser); 1.382 + } 1.383 + break; 1.384 + } 1.385 + case "TabRemove": 1.386 + this.updateTabTelemetryVars(window); 1.387 + break; 1.388 + case "TabSelect": { 1.389 + let browser = aEvent.originalTarget.linkedBrowser; 1.390 + this.onTabSelect(window, browser); 1.391 + break; 1.392 + } 1.393 + } 1.394 + }, 1.395 + 1.396 + receiveMessage: function ss_receiveMessage(aMessage) { 1.397 + let browser = aMessage.target; 1.398 + switch (aMessage.name) { 1.399 + case "SessionStore:collectFormdata": 1.400 + browser.__SS_data.formdata = aMessage.json.data; 1.401 + break; 1.402 + case "SessionStore:collectScrollPosition": 1.403 + browser.__SS_data.scroll = aMessage.json.data; 1.404 + break; 1.405 + default: 1.406 + let window = aMessage.target.ownerDocument.defaultView; 1.407 + this.onTabLoad(window, aMessage.target, aMessage); 1.408 + break; 1.409 + } 1.410 + }, 1.411 + 1.412 + onWindowOpen: function ss_onWindowOpen(aWindow) { 1.413 + // Return if window has already been initialized 1.414 + if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) 1.415 + return; 1.416 + 1.417 + // Ignore non-browser windows and windows opened while shutting down 1.418 + if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING) 1.419 + return; 1.420 + 1.421 + // Assign it a unique identifier and create its data object 1.422 + aWindow.__SSID = "window" + gUUIDGenerator.generateUUID().toString(); 1.423 + this._windows[aWindow.__SSID] = { tabs: [], selected: 0, _closedTabs: [] }; 1.424 + this._orderedWindows.push(aWindow.__SSID); 1.425 + 1.426 + // Perform additional initialization when the first window is loading 1.427 + if (this._loadState == STATE_STOPPED) { 1.428 + this._loadState = STATE_RUNNING; 1.429 + this._lastSaveTime = Date.now(); 1.430 + 1.431 + // Nothing to restore, notify observers things are complete 1.432 + if (!this.shouldRestore()) { 1.433 + this._clearCache(); 1.434 + Services.obs.notifyObservers(null, "sessionstore-windows-restored", ""); 1.435 + } 1.436 + } 1.437 + 1.438 + // Add tab change listeners to all already existing tabs 1.439 + let tabs = aWindow.Browser.tabs; 1.440 + for (let i = 0; i < tabs.length; i++) 1.441 + this.onTabAdd(aWindow, tabs[i].browser, true); 1.442 + 1.443 + // Notification of tab add/remove/selection 1.444 + let tabContainer = aWindow.document.getElementById("tabs"); 1.445 + tabContainer.addEventListener("TabOpen", this, true); 1.446 + tabContainer.addEventListener("TabClose", this, true); 1.447 + tabContainer.addEventListener("TabRemove", this, true); 1.448 + tabContainer.addEventListener("TabSelect", this, true); 1.449 + }, 1.450 + 1.451 + onWindowClose: function ss_onWindowClose(aWindow) { 1.452 + // Ignore windows not tracked by SessionStore 1.453 + if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) 1.454 + return; 1.455 + 1.456 + let tabContainer = aWindow.document.getElementById("tabs"); 1.457 + tabContainer.removeEventListener("TabOpen", this, true); 1.458 + tabContainer.removeEventListener("TabClose", this, true); 1.459 + tabContainer.removeEventListener("TabRemove", this, true); 1.460 + tabContainer.removeEventListener("TabSelect", this, true); 1.461 + 1.462 + if (this._loadState == STATE_RUNNING) { 1.463 + // Update all window data for a last time 1.464 + this._collectWindowData(aWindow); 1.465 + 1.466 + // Clear this window from the list 1.467 + delete this._windows[aWindow.__SSID]; 1.468 + 1.469 + // Save the state without this window to disk 1.470 + this.saveStateDelayed(); 1.471 + } 1.472 + 1.473 + let tabs = aWindow.Browser.tabs; 1.474 + for (let i = 0; i < tabs.length; i++) 1.475 + this.onTabRemove(aWindow, tabs[i].browser, true); 1.476 + 1.477 + delete aWindow.__SSID; 1.478 + }, 1.479 + 1.480 + onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) { 1.481 + aBrowser.messageManager.addMessageListener("pageshow", this); 1.482 + aBrowser.messageManager.addMessageListener("Content:SessionHistory", this); 1.483 + aBrowser.messageManager.addMessageListener("SessionStore:collectFormdata", this); 1.484 + aBrowser.messageManager.addMessageListener("SessionStore:collectScrollPosition", this); 1.485 + 1.486 + if (!aNoNotification) 1.487 + this.saveStateDelayed(); 1.488 + this._updateCrashReportURL(aWindow); 1.489 + }, 1.490 + 1.491 + onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) { 1.492 + aBrowser.messageManager.removeMessageListener("pageshow", this); 1.493 + aBrowser.messageManager.removeMessageListener("Content:SessionHistory", this); 1.494 + aBrowser.messageManager.removeMessageListener("SessionStore:collectFormdata", this); 1.495 + aBrowser.messageManager.removeMessageListener("SessionStore:collectScrollPosition", this); 1.496 + 1.497 + // If this browser is being restored, skip any session save activity 1.498 + if (aBrowser.__SS_restore) 1.499 + return; 1.500 + 1.501 + delete aBrowser.__SS_data; 1.502 + 1.503 + if (!aNoNotification) 1.504 + this.saveStateDelayed(); 1.505 + }, 1.506 + 1.507 + onTabClose: function ss_onTabClose(aWindow, aBrowser) { 1.508 + if (this._maxTabsUndo == 0) 1.509 + return; 1.510 + 1.511 + if (aWindow.Browser.tabs.length > 0) { 1.512 + // Bundle this browser's data and extra data and save in the closedTabs 1.513 + // window property 1.514 + // 1.515 + // NB: The access to aBrowser.__SS_extdata throws during automation (in 1.516 + // browser_msgmgr_01). See bug 888736. 1.517 + let data = aBrowser.__SS_data; 1.518 + if (!data) { 1.519 + return; // Cannot restore an empty tab. 1.520 + } 1.521 + try { data.extData = aBrowser.__SS_extdata; } catch (e) { } 1.522 + 1.523 + this._windows[aWindow.__SSID]._closedTabs.unshift({ state: data }); 1.524 + let length = this._windows[aWindow.__SSID]._closedTabs.length; 1.525 + if (length > this._maxTabsUndo) 1.526 + this._windows[aWindow.__SSID]._closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo); 1.527 + } 1.528 + }, 1.529 + 1.530 + onTabLoad: function ss_onTabLoad(aWindow, aBrowser, aMessage) { 1.531 + // If this browser is being restored, skip any session save activity 1.532 + if (aBrowser.__SS_restore) 1.533 + return; 1.534 + 1.535 + // Ignore a transient "about:blank" 1.536 + if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") 1.537 + return; 1.538 + 1.539 + if (aMessage.name == "Content:SessionHistory") { 1.540 + delete aBrowser.__SS_data; 1.541 + this._collectTabData(aBrowser, aMessage.json); 1.542 + } 1.543 + 1.544 + // Save out the state as quickly as possible 1.545 + if (aMessage.name == "pageshow") 1.546 + this.saveStateNow(); 1.547 + 1.548 + this._updateCrashReportURL(aWindow); 1.549 + }, 1.550 + 1.551 + onTabSelect: function ss_onTabSelect(aWindow, aBrowser) { 1.552 + if (this._loadState != STATE_RUNNING) 1.553 + return; 1.554 + 1.555 + let index = aWindow.Elements.browsers.selectedIndex; 1.556 + this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based 1.557 + 1.558 + // Restore the resurrected browser 1.559 + if (aBrowser.__SS_restore) { 1.560 + let data = aBrowser.__SS_data; 1.561 + if (data.entries.length > 0) { 1.562 + let json = { 1.563 + uri: data.entries[data.index - 1].url, 1.564 + flags: null, 1.565 + entries: data.entries, 1.566 + index: data.index 1.567 + }; 1.568 + aBrowser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json); 1.569 + } 1.570 + 1.571 + delete aBrowser.__SS_restore; 1.572 + } 1.573 + 1.574 + this._updateCrashReportURL(aWindow); 1.575 + }, 1.576 + 1.577 + saveStateDelayed: function ss_saveStateDelayed() { 1.578 + if (!this._saveTimer) { 1.579 + // Interval until the next disk operation is allowed 1.580 + let minimalDelay = this._lastSaveTime + this._interval - Date.now(); 1.581 + 1.582 + // If we have to wait, set a timer, otherwise saveState directly 1.583 + let delay = Math.max(minimalDelay, 2000); 1.584 + if (delay > 0) { 1.585 + this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.586 + this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); 1.587 + } else { 1.588 + this.saveState(); 1.589 + } 1.590 + } 1.591 + }, 1.592 + 1.593 + saveStateNow: function ss_saveStateNow() { 1.594 + // Kill any queued timer and save immediately 1.595 + if (this._saveTimer) { 1.596 + this._saveTimer.cancel(); 1.597 + this._saveTimer = null; 1.598 + } 1.599 + this.saveState(); 1.600 + }, 1.601 + 1.602 + saveState: function ss_saveState() { 1.603 + let data = this._getCurrentState(); 1.604 + // sanity check before we overwrite the session file 1.605 + if (data.windows && data.windows.length && data.selectedWindow) { 1.606 + this._writeFile(this._sessionFile, JSON.stringify(data)); 1.607 + 1.608 + this._lastSaveTime = Date.now(); 1.609 + } else { 1.610 + dump("SessionStore: Not saving state with invalid data: " + JSON.stringify(data) + "\n"); 1.611 + } 1.612 + }, 1.613 + 1.614 + _getCurrentState: function ss_getCurrentState() { 1.615 + let self = this; 1.616 + this._forEachBrowserWindow(function(aWindow) { 1.617 + self._collectWindowData(aWindow); 1.618 + }); 1.619 + 1.620 + let data = { windows: [] }; 1.621 + for (let i = 0; i < this._orderedWindows.length; i++) 1.622 + data.windows.push(this._windows[this._orderedWindows[i]]); 1.623 + data.selectedWindow = this._selectedWindow; 1.624 + return data; 1.625 + }, 1.626 + 1.627 + _collectTabData: function ss__collectTabData(aBrowser, aHistory) { 1.628 + // If this browser is being restored, skip any session save activity 1.629 + if (aBrowser.__SS_restore) 1.630 + return; 1.631 + 1.632 + let aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 }; 1.633 + 1.634 + let tabData = {}; 1.635 + tabData.entries = aHistory.entries; 1.636 + tabData.index = aHistory.index; 1.637 + tabData.attributes = { image: aBrowser.mIconURL }; 1.638 + 1.639 + aBrowser.__SS_data = tabData; 1.640 + }, 1.641 + 1.642 + _getTabData: function(aWindow) { 1.643 + return aWindow.Browser.tabs 1.644 + .filter(tab => !tab.isPrivate && tab.browser.__SS_data) 1.645 + .map(tab => { 1.646 + let browser = tab.browser; 1.647 + let tabData = browser.__SS_data; 1.648 + if (browser.__SS_extdata) 1.649 + tabData.extData = browser.__SS_extdata; 1.650 + return tabData; 1.651 + }); 1.652 + }, 1.653 + 1.654 + _collectWindowData: function ss__collectWindowData(aWindow) { 1.655 + // Ignore windows not tracked by SessionStore 1.656 + if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) 1.657 + return; 1.658 + 1.659 + let winData = this._windows[aWindow.__SSID]; 1.660 + 1.661 + let index = aWindow.Elements.browsers.selectedIndex; 1.662 + winData.selected = parseInt(index) + 1; // 1-based 1.663 + 1.664 + let tabData = this._getTabData(aWindow); 1.665 + winData.tabs = tabData.concat(this._tabsFromOtherGroups); 1.666 + }, 1.667 + 1.668 + _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) { 1.669 + let windowsEnum = Services.wm.getEnumerator("navigator:browser"); 1.670 + while (windowsEnum.hasMoreElements()) { 1.671 + let window = windowsEnum.getNext(); 1.672 + if (window.__SSID && !window.closed) 1.673 + aFunc.call(this, window); 1.674 + } 1.675 + }, 1.676 + 1.677 + _writeFile: function ss_writeFile(aFile, aData) { 1.678 + let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); 1.679 + stateString.data = aData; 1.680 + Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); 1.681 + 1.682 + // Don't touch the file if an observer has deleted all state data 1.683 + if (!stateString.data) 1.684 + return; 1.685 + 1.686 + // Initialize the file output stream. 1.687 + let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); 1.688 + ostream.init(aFile, 0x02 | 0x08 | 0x20, 0600, ostream.DEFER_OPEN); 1.689 + 1.690 + // Obtain a converter to convert our data to a UTF-8 encoded input stream. 1.691 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); 1.692 + converter.charset = "UTF-8"; 1.693 + 1.694 + // Asynchronously copy the data to the file. 1.695 + let istream = converter.convertToInputStream(aData); 1.696 + NetUtil.asyncCopy(istream, ostream, function(rc) { 1.697 + if (Components.isSuccessCode(rc)) { 1.698 + if (Services.startup.shuttingDown) { 1.699 + Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", ""); 1.700 + } 1.701 + Services.obs.notifyObservers(null, "sessionstore-state-write-complete", ""); 1.702 + } 1.703 + }); 1.704 + }, 1.705 + 1.706 + _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) { 1.707 +#ifdef MOZ_CRASHREPORTER 1.708 + try { 1.709 + let currentURI = aWindow.Browser.selectedBrowser.currentURI.clone(); 1.710 + // if the current URI contains a username/password, remove it 1.711 + try { 1.712 + currentURI.userPass = ""; 1.713 + } 1.714 + catch (ex) { } // ignore failures on about: URIs 1.715 + 1.716 + CrashReporter.annotateCrashReport("URL", currentURI.spec); 1.717 + } 1.718 + catch (ex) { 1.719 + // don't make noise when crashreporter is built but not enabled 1.720 + if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) 1.721 + Components.utils.reportError("SessionStore:" + ex); 1.722 + } 1.723 +#endif 1.724 + }, 1.725 + 1.726 + getBrowserState: function ss_getBrowserState() { 1.727 + let data = this._getCurrentState(); 1.728 + return JSON.stringify(data); 1.729 + }, 1.730 + 1.731 + getClosedTabCount: function ss_getClosedTabCount(aWindow) { 1.732 + if (!aWindow || !aWindow.__SSID) 1.733 + return 0; // not a browser window, or not otherwise tracked by SS. 1.734 + 1.735 + return this._windows[aWindow.__SSID]._closedTabs.length; 1.736 + }, 1.737 + 1.738 + getClosedTabData: function ss_getClosedTabData(aWindow) { 1.739 + if (!aWindow.__SSID) 1.740 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.741 + 1.742 + return JSON.stringify(this._windows[aWindow.__SSID]._closedTabs); 1.743 + }, 1.744 + 1.745 + undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { 1.746 + if (!aWindow.__SSID) 1.747 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.748 + 1.749 + let closedTabs = this._windows[aWindow.__SSID]._closedTabs; 1.750 + if (!closedTabs) 1.751 + return null; 1.752 + 1.753 + // default to the most-recently closed tab 1.754 + aIndex = aIndex || 0; 1.755 + if (!(aIndex in closedTabs)) 1.756 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.757 + 1.758 + // fetch the data of closed tab, while removing it from the array 1.759 + let closedTab = closedTabs.splice(aIndex, 1).shift(); 1.760 + 1.761 + // create a new tab and bring to front 1.762 + let tab = aWindow.Browser.addTab(closedTab.state.entries[closedTab.state.index - 1].url, true); 1.763 + 1.764 + tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", { 1.765 + uri: closedTab.state.entries[closedTab.state.index - 1].url, 1.766 + flags: null, 1.767 + entries: closedTab.state.entries, 1.768 + index: closedTab.state.index 1.769 + }); 1.770 + 1.771 + // Put back the extra data 1.772 + tab.browser.__SS_extdata = closedTab.extData; 1.773 + 1.774 + return tab.chromeTab; 1.775 + }, 1.776 + 1.777 + forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) { 1.778 + if (!aWindow.__SSID) 1.779 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.780 + 1.781 + let closedTabs = this._windows[aWindow.__SSID]._closedTabs; 1.782 + 1.783 + // default to the most-recently closed tab 1.784 + aIndex = aIndex || 0; 1.785 + if (!(aIndex in closedTabs)) 1.786 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.787 + 1.788 + // remove closed tab from the array 1.789 + closedTabs.splice(aIndex, 1); 1.790 + }, 1.791 + 1.792 + getTabValue: function ss_getTabValue(aTab, aKey) { 1.793 + let browser = aTab.linkedBrowser; 1.794 + let data = browser.__SS_extdata || {}; 1.795 + return data[aKey] || ""; 1.796 + }, 1.797 + 1.798 + setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { 1.799 + let browser = aTab.linkedBrowser; 1.800 + 1.801 + // Thumbnails are actually stored in the cache, so do the save and update the URI 1.802 + if (aKey == "thumbnail") { 1.803 + let file = this._sessionCache.clone(); 1.804 + file.append("thumbnail-" + browser.contentWindowId); 1.805 + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); 1.806 + 1.807 + let source = Services.io.newURI(aStringValue, "UTF8", null); 1.808 + let target = Services.io.newFileURI(file) 1.809 + 1.810 + let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist); 1.811 + persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; 1.812 + persist.saveURI(source, null, null, null, null, file); 1.813 + 1.814 + aStringValue = target.spec; 1.815 + } 1.816 + 1.817 + if (!browser.__SS_extdata) 1.818 + browser.__SS_extdata = {}; 1.819 + browser.__SS_extdata[aKey] = aStringValue; 1.820 + this.saveStateDelayed(); 1.821 + }, 1.822 + 1.823 + deleteTabValue: function ss_deleteTabValue(aTab, aKey) { 1.824 + let browser = aTab.linkedBrowser; 1.825 + if (browser.__SS_extdata && browser.__SS_extdata[aKey]) 1.826 + delete browser.__SS_extdata[aKey]; 1.827 + else 1.828 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.829 + }, 1.830 + 1.831 + shouldRestore: function ss_shouldRestore() { 1.832 + return this._shouldRestore || (3 == Services.prefs.getIntPref("browser.startup.page")); 1.833 + }, 1.834 + 1.835 + restoreLastSession: function ss_restoreLastSession(aBringToFront) { 1.836 + let self = this; 1.837 + function notifyObservers(aMessage) { 1.838 + self._clearCache(); 1.839 + Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || ""); 1.840 + } 1.841 + 1.842 + // The previous session data has already been renamed to the backup file 1.843 + if (!this._sessionFileBackup.exists()) { 1.844 + notifyObservers("fail") 1.845 + return; 1.846 + } 1.847 + 1.848 + try { 1.849 + let channel = NetUtil.newChannel(this._sessionFileBackup); 1.850 + channel.contentType = "application/json"; 1.851 + NetUtil.asyncFetch(channel, function(aStream, aResult) { 1.852 + if (!Components.isSuccessCode(aResult)) { 1.853 + Cu.reportError("SessionStore: Could not read from sessionstore.bak file"); 1.854 + notifyObservers("fail"); 1.855 + return; 1.856 + } 1.857 + 1.858 + // Read session state file into a string and let observers modify the state before it's being used 1.859 + let state = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); 1.860 + state.data = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || ""; 1.861 + aStream.close(); 1.862 + 1.863 + Services.obs.notifyObservers(state, "sessionstore-state-read", ""); 1.864 + 1.865 + let data = null; 1.866 + try { 1.867 + data = JSON.parse(state.data); 1.868 + } catch (ex) { 1.869 + Cu.reportError("SessionStore: Could not parse JSON: " + ex); 1.870 + } 1.871 + 1.872 + if (!data || data.windows.length == 0) { 1.873 + notifyObservers("fail"); 1.874 + return; 1.875 + } 1.876 + 1.877 + let window = Services.wm.getMostRecentWindow("navigator:browser"); 1.878 + 1.879 + if (typeof data.selectedWindow == "number") { 1.880 + this._selectedWindow = data.selectedWindow; 1.881 + } 1.882 + let windowIndex = this._selectedWindow - 1; 1.883 + let tabs = data.windows[windowIndex].tabs; 1.884 + let selected = data.windows[windowIndex].selected; 1.885 + 1.886 + let currentGroupId; 1.887 + try { 1.888 + currentGroupId = JSON.parse(data.windows[windowIndex].extData["tabview-groups"]).activeGroupId; 1.889 + } catch (ex) { /* currentGroupId is undefined if user has no tab groups */ } 1.890 + 1.891 + // Move all window data from sessionstore.js to this._windows. 1.892 + this._orderedWindows = []; 1.893 + for (let i = 0; i < data.windows.length; i++) { 1.894 + let SSID; 1.895 + if (i != windowIndex) { 1.896 + SSID = "window" + gUUIDGenerator.generateUUID().toString(); 1.897 + this._windows[SSID] = data.windows[i]; 1.898 + } else { 1.899 + SSID = window.__SSID; 1.900 + this._windows[SSID].extData = data.windows[i].extData; 1.901 + this._windows[SSID]._closedTabs = 1.902 + this._windows[SSID]._closedTabs.concat(data.windows[i]._closedTabs); 1.903 + } 1.904 + this._orderedWindows.push(SSID); 1.905 + } 1.906 + 1.907 + if (selected > tabs.length) // Clamp the selected index if it's bogus 1.908 + selected = 1; 1.909 + 1.910 + for (let i=0; i<tabs.length; i++) { 1.911 + let tabData = tabs[i]; 1.912 + let tabGroupId = (typeof currentGroupId == "number") ? 1.913 + JSON.parse(tabData.extData["tabview-tab"]).groupID : null; 1.914 + 1.915 + if (tabGroupId && tabGroupId != currentGroupId) { 1.916 + this._tabsFromOtherGroups.push(tabData); 1.917 + continue; 1.918 + } 1.919 + 1.920 + // We must have selected tabs as soon as possible, so we let all tabs be selected 1.921 + // until we get the real selected tab. Then we stop selecting tabs. The end result 1.922 + // is that the right tab is selected, but we also don't get a bunch of errors 1.923 + let bringToFront = (i + 1 <= selected) && aBringToFront; 1.924 + let tab = window.Browser.addTab(tabData.entries[tabData.index - 1].url, bringToFront); 1.925 + 1.926 + // Start a real load for the selected tab 1.927 + if (i + 1 == selected) { 1.928 + let json = { 1.929 + uri: tabData.entries[tabData.index - 1].url, 1.930 + flags: null, 1.931 + entries: tabData.entries, 1.932 + index: tabData.index 1.933 + }; 1.934 + tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json); 1.935 + } else { 1.936 + // Make sure the browser has its session data for the delay reload 1.937 + tab.browser.__SS_data = tabData; 1.938 + tab.browser.__SS_restore = true; 1.939 + 1.940 + // Restore current title 1.941 + tab.chromeTab.updateTitle(tabData.entries[tabData.index - 1].title); 1.942 + } 1.943 + 1.944 + tab.browser.__SS_tabFormData = tabData 1.945 + tab.browser.__SS_extdata = tabData.extData; 1.946 + } 1.947 + 1.948 + notifyObservers(); 1.949 + }.bind(this)); 1.950 + } catch (ex) { 1.951 + Cu.reportError("SessionStore: Could not read from sessionstore.bak file: " + ex); 1.952 + notifyObservers("fail"); 1.953 + } 1.954 + } 1.955 +}; 1.956 + 1.957 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);