--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -21,16 +21,17 @@ const TAB_STATE_WILL_RESTORE = 3;
// A new window has just been restored. At this stage, tabs are generally
// not restored.
const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
const NOTIFY_INITIATING_MANUAL_RESTORE = "sessionstore-initiating-manual-restore";
+const NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
// Maximum number of tabs to restore simultaneously. Previously controlled by
// the browser.sessionstore.max_concurrent_tabs pref.
const MAX_CONCURRENT_TAB_RESTORES = 3;
// Amount (in CSS px) by which we allow window edges to be off-screen
@@ -164,16 +165,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
"resource://devtools/client/scratchpad/scratchpad-manager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
"resource:///modules/sessionstore/SessionSaver.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies",
"resource:///modules/sessionstore/SessionCookies.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
"resource:///modules/sessionstore/SessionFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+ "resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
"resource:///modules/sessionstore/TabAttributes.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
"resource:///modules/ContentCrashHandlers.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabState",
"resource:///modules/sessionstore/TabState.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
"resource:///modules/sessionstore/TabStateCache.jsm");
@@ -462,16 +465,19 @@ var SessionStoreInternal = {
// number of tabs currently restoring
_tabsRestoringCount: 0,
// When starting Firefox with a single private window, this is the place
// where we keep the session we actually wanted to restore in case the user
// decides to later open a non-private window as well.
_deferredInitialState: null,
+ // Keeps track of whether a notification needs to be sent that closed objects have changed.
+ _closedObjectsChanged: false,
+
// A promise resolved once initialization is complete
_deferredInitialized: (function () {
let deferred = {};
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
@@ -677,39 +683,45 @@ var SessionStoreInternal = {
* Handle notifications
*/
observe: function ssi_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "browser-window-before-show": // catch new windows
this.onBeforeBrowserWindowShown(aSubject);
break;
case "domwindowclosed": // catch closed windows
- this.onClose(aSubject);
+ this.onClose(aSubject).then(() => {
+ this._notifyOfClosedObjectsChange();
+ });
break;
case "quit-application-granted":
let syncShutdown = aData == "syncShutdown";
this.onQuitApplicationGranted(syncShutdown);
break;
case "browser-lastwindow-close-granted":
this.onLastWindowCloseGranted();
break;
case "quit-application":
this.onQuitApplication(aData);
break;
case "browser:purge-session-history": // catch sanitization
this.onPurgeSessionHistory();
+ this._notifyOfClosedObjectsChange();
break;
case "browser:purge-domain-data":
this.onPurgeDomainData(aData);
+ this._notifyOfClosedObjectsChange();
break;
case "nsPref:changed": // catch pref changes
this.onPrefChange(aData);
+ this._notifyOfClosedObjectsChange();
break;
case "idle-daily":
this.onIdleDaily();
+ this._notifyOfClosedObjectsChange();
break;
}
},
/**
* This method handles incoming messages sent by the session store content
* script via the Frame Message Manager or Parent Process Message Manager,
* and thus enables communication with OOP tabs.
@@ -939,16 +951,17 @@ var SessionStoreInternal = {
this.onTabBrowserInserted(win, target);
break;
case "TabClose":
// `adoptedBy` will be set if the tab was closed because it is being
// moved to a new window.
if (!aEvent.detail.adoptedBy)
this.onTabClose(win, target);
this.onTabRemove(win, target);
+ this._notifyOfClosedObjectsChange();
break;
case "TabSelect":
this.onTabSelect(win);
break;
case "TabShow":
this.onTabShow(win, target);
break;
case "TabHide":
@@ -1155,28 +1168,28 @@ var SessionStoreInternal = {
// These are our pinned tabs, which we should restore
if (appTabsState.windows.length) {
newWindowState = appTabsState.windows[0];
delete newWindowState.__lastSessionWindowID;
}
// In case there were no unpinned tabs, remove the window from _closedWindows
if (!normalTabsState.windows.length) {
- this._closedWindows.splice(closedWindowIndex, 1);
+ this._removeClosedWindow(closedWindowIndex);
}
// Or update _closedWindows with the modified state
else {
delete normalTabsState.windows[0].__lastSessionWindowID;
this._closedWindows[closedWindowIndex] = normalTabsState.windows[0];
}
}
else {
// If we're just restoring the window, make sure it gets removed from
// _closedWindows.
- this._closedWindows.splice(closedWindowIndex, 1);
+ this._removeClosedWindow(closedWindowIndex);
newWindowState = closedWindowState;
delete newWindowState.hidden;
}
if (newWindowState) {
// Ensure that the window state isn't hidden
this._restoreCount = 1;
let state = { windows: [newWindowState] };
@@ -1261,33 +1274,36 @@ var SessionStoreInternal = {
},
/**
* On window close...
* - remove event listeners from tabs
* - save all window data
* @param aWindow
* Window reference
+ *
+ * @returns a Promise
*/
onClose: function ssi_onClose(aWindow) {
+ let completionPromise = Promise.resolve();
// this window was about to be restored - conserve its original data, if any
let isFullyLoaded = this._isWindowLoaded(aWindow);
if (!isFullyLoaded) {
if (!aWindow.__SSi) {
aWindow.__SSi = this._generateWindowID();
}
this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
delete this._statesToRestore[aWindow.__SS_restoreID];
delete aWindow.__SS_restoreID;
}
// ignore windows not tracked by SessionStore
if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
- return;
+ return completionPromise;
}
// notify that the session store will stop tracking this window so that
// extensions can store any data about this window in session store before
// that's not possible anymore
let event = aWindow.document.createEvent("Events");
event.initEvent("SSWindowClosing", true, false);
aWindow.dispatchEvent(event);
@@ -1372,17 +1388,17 @@ var SessionStoreInternal = {
// 3) When the flush is complete, revisit our decision to store the window
// in _closedWindows, and add/remove as necessary.
if (!winData.isPrivate) {
// Remove any open private tabs the window may contain.
PrivacyFilter.filterPrivateTabs(winData);
this.maybeSaveClosedWindow(winData, isLastWindow);
}
- TabStateFlusher.flushWindow(aWindow).then(() => {
+ completionPromise = TabStateFlusher.flushWindow(aWindow).then(() => {
// At this point, aWindow is closed! You should probably not try to
// access any DOM elements from aWindow within this callback unless
// you're holding on to them in the closure.
for (let browser of browsers) {
if (this._closedWindowTabs.has(browser.permanentKey)) {
let tabData = this._closedWindowTabs.get(browser.permanentKey);
TabState.copyFromCache(browser, tabData);
@@ -1408,16 +1424,18 @@ var SessionStoreInternal = {
});
} else {
this.cleanUpWindow(aWindow, winData, browsers);
}
for (let i = 0; i < tabbrowser.tabs.length; i++) {
this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
}
+
+ return completionPromise;
},
/**
* Clean up the message listeners on a window that has finally
* gone away. Call this once you're sure you don't want to hear
* from any of this windows tabs from here forward.
*
* @param aWindow
@@ -1486,18 +1504,19 @@ var SessionStoreInternal = {
}
// About to save the closed window, add a unique ID.
winData.closedId = this._nextClosedId++;
// Insert tabData at the right position.
this._closedWindows.splice(index, 0, winData);
this._capClosedWindows();
+ this._closedObjectsChanged = true;
} else if (!shouldStore && alreadyStored) {
- this._closedWindows.splice(winIndex, 1);
+ this._removeClosedWindow(winIndex);
}
}
},
/**
* On quit application granted
*/
onQuitApplicationGranted: function ssi_onQuitApplicationGranted(syncShutdown=false) {
@@ -1664,23 +1683,29 @@ var SessionStoreInternal = {
let openWindows = {};
// Collect open windows.
this._forEachBrowserWindow(({__SSi: id}) => openWindows[id] = true);
// also clear all data about closed tabs and windows
for (let ix in this._windows) {
if (ix in openWindows) {
- this._windows[ix]._closedTabs = [];
+ if (this._windows[ix]._closedTabs.length) {
+ this._windows[ix]._closedTabs = [];
+ this._closedObjectsChanged = true;
+ }
} else {
delete this._windows[ix];
}
}
// also clear all data about closed windows
- this._closedWindows = [];
+ if (this._closedWindows.length) {
+ this._closedWindows = [];
+ this._closedObjectsChanged = true;
+ }
// give the tabbrowsers a chance to clear their histories first
var win = this._getMostRecentBrowserWindow();
if (win) {
win.setTimeout(() => SessionSaver.run(), 0);
} else if (RunState.isRunning) {
SessionSaver.run();
}
@@ -1700,18 +1725,20 @@ var SessionStoreInternal = {
return true;
}
return aEntry.children && aEntry.children.some(containsDomain, this);
}
// remove all closed tabs containing a reference to the given domain
for (let ix in this._windows) {
let closedTabs = this._windows[ix]._closedTabs;
for (let i = closedTabs.length - 1; i >= 0; i--) {
- if (closedTabs[i].state.entries.some(containsDomain, this))
+ if (closedTabs[i].state.entries.some(containsDomain, this)) {
closedTabs.splice(i, 1);
+ this._closedObjectsChanged = true;
+ }
}
}
// remove all open & closed tabs containing a reference to the given
// domain in closed windows
for (let ix = this._closedWindows.length - 1; ix >= 0; ix--) {
let closedTabs = this._closedWindows[ix]._closedTabs;
let openTabs = this._closedWindows[ix].tabs;
let openTabCount = openTabs.length;
@@ -1753,17 +1780,20 @@ var SessionStoreInternal = {
*/
onPrefChange: function ssi_onPrefChange(aData) {
switch (aData) {
// if the user decreases the max number of closed tabs they want
// preserved update our internal states to match that max
case "sessionstore.max_tabs_undo":
this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
for (let ix in this._windows) {
- this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
+ if (this._windows[ix]._closedTabs.length > this._max_tabs_undo) {
+ this._windows[ix]._closedTabs.splice(this._max_tabs_undo, this._windows[ix]._closedTabs.length);
+ this._closedObjectsChanged = true;
+ }
}
break;
case "sessionstore.max_windows_undo":
this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
this._capClosedWindows();
break;
}
},
@@ -1909,16 +1939,17 @@ var SessionStoreInternal = {
index = closedTabs.length;
}
// About to save the closed tab, add a unique ID.
tabData.closedId = this._nextClosedId++;
// Insert tabData at the right position.
closedTabs.splice(index, 0, tabData);
+ this._closedObjectsChanged = true;
// Truncate the list of closed tabs, if needed.
if (closedTabs.length > this._max_tabs_undo) {
closedTabs.splice(this._max_tabs_undo, closedTabs.length);
}
},
/**
@@ -1929,16 +1960,17 @@ var SessionStoreInternal = {
* @param closedTabs (array)
* The list of closed tabs for a window.
* @param index (uint)
* The index of the tab to remove.
*/
removeClosedTabData(closedTabs, index) {
// Remove the given index from the list.
let [closedTab] = closedTabs.splice(index, 1);
+ this._closedObjectsChanged = true;
// If the closed tab's state still has a .permanentKey property then we
// haven't seen its final update message yet. Remove it from the map of
// closed tabs so that we will simply discard its last messages and will
// not add it back to the list of closed tabs again.
if (closedTab.permanentKey) {
this._closedTabs.delete(closedTab.permanentKey);
this._closedWindowTabs.delete(closedTab.permanentKey);
@@ -2058,32 +2090,35 @@ var SessionStoreInternal = {
// Remove old closed windows
this._cleanupOldData([this._closedWindows]);
// Remove closed tabs of closed windows
this._cleanupOldData(this._closedWindows.map((winData) => winData._closedTabs));
// Remove closed tabs of open windows
this._cleanupOldData(Object.keys(this._windows).map((key) => this._windows[key]._closedTabs));
+
+ this._notifyOfClosedObjectsChange();
},
// Remove "old" data from an array
_cleanupOldData: function(targets) {
const TIME_TO_LIVE = this._prefBranch.getIntPref("sessionstore.cleanup.forget_closed_after");
const now = Date.now();
for (let array of targets) {
for (let i = array.length - 1; i >= 0; --i) {
let data = array[i];
// Make sure that we have a timestamp to tell us when the target
// has been closed. If we don't have a timestamp, default to a
// safe timestamp: just now.
data.closedAt = data.closedAt || now;
if (now - data.closedAt > TIME_TO_LIVE) {
array.splice(i, 1);
+ this._closedObjectsChanged = true;
}
}
}
},
/* ........ nsISessionStore API .............. */
getBrowserState: function ssi_getBrowserState() {
@@ -2128,27 +2163,33 @@ var SessionStoreInternal = {
this._forEachBrowserWindow(function(aWindow) {
if (aWindow != window) {
aWindow.close();
this.onClose(aWindow);
}
});
// make sure closed window data isn't kept
- this._closedWindows = [];
+ if (this._closedWindows.length) {
+ this._closedWindows = [];
+ this._closedObjectsChanged = true;
+ }
// determine how many windows are meant to be restored
this._restoreCount = state.windows ? state.windows.length : 0;
// global data must be restored before restoreWindow is called so that
// it happens before observers are notified
this._globalState.setFromState(state);
// restore to the given state
this.restoreWindows(window, state, {overwriteTabs: true});
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
},
getWindowState: function ssi_getWindowState(aWindow) {
if ("__SSi" in aWindow) {
return JSON.stringify(this._getWindowState(aWindow));
}
if (DyingWindowCache.has(aWindow)) {
@@ -2160,16 +2201,19 @@ var SessionStoreInternal = {
},
setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
if (!aWindow.__SSi) {
throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
}
this.restoreWindows(aWindow, aState, {overwriteTabs: aOverwrite});
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
},
getTabState: function ssi_getTabState(aTab) {
if (!aTab.ownerGlobal.__SSi) {
throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
}
let tabState = TabState.collect(aTab);
@@ -2198,16 +2242,19 @@ var SessionStoreInternal = {
throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
}
if (aTab.linkedBrowser.__SS_restoreState) {
this._resetTabRestoringState(aTab);
}
this.restoreTab(aTab, tabState);
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
},
duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0, aRestoreImmediately = true) {
if (!aTab.ownerGlobal.__SSi) {
throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
}
if (!aWindow.gBrowser) {
throw Components.Exception("Invalid window object: no gBrowser", Cr.NS_ERROR_INVALID_ARG);
@@ -2312,16 +2359,19 @@ var SessionStoreInternal = {
this.restoreTab(tab, state);
// restore the tab's position
tabbrowser.moveTabTo(tab, pos);
// focus the tab's content area (bug 342432)
tab.linkedBrowser.focus();
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
+
return tab;
},
forgetClosedTab: function ssi_forgetClosedTab(aWindow, aIndex) {
if (!aWindow.__SSi) {
throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
}
@@ -2330,51 +2380,61 @@ var SessionStoreInternal = {
// default to the most-recently closed tab
aIndex = aIndex || 0;
if (!(aIndex in closedTabs)) {
throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
}
// remove closed tab from the array
this.removeClosedTabData(closedTabs, aIndex);
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
},
getClosedWindowCount: function ssi_getClosedWindowCount() {
return this._closedWindows.length;
},
getClosedWindowData: function ssi_getClosedWindowData(aAsString = true) {
return aAsString ? JSON.stringify(this._closedWindows) : Cu.cloneInto(this._closedWindows, {});
},
undoCloseWindow: function ssi_undoCloseWindow(aIndex) {
if (!(aIndex in this._closedWindows)) {
throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
}
// reopen the window
- let state = { windows: this._closedWindows.splice(aIndex, 1) };
+ let state = { windows: this._removeClosedWindow(aIndex) };
delete state.windows[0].closedAt; // Window is now open.
let window = this._openWindowWithState(state);
this.windowToFocus = window;
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
+
return window;
},
forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
// default to the most-recently closed window
aIndex = aIndex || 0;
if (!(aIndex in this._closedWindows)) {
throw Components.Exception("Invalid index: not in the closed windows", Cr.NS_ERROR_INVALID_ARG);
}
// remove closed window from the array
let winData = this._closedWindows[aIndex];
- this._closedWindows.splice(aIndex, 1);
+ this._removeClosedWindow(aIndex);
this._saveableClosedWindowData.delete(winData);
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
},
getWindowValue: function ssi_getWindowValue(aWindow, aKey) {
if ("__SSi" in aWindow) {
var data = this._windows[aWindow.__SSi].extData || {};
return data[aKey] || "";
}
@@ -2579,30 +2639,34 @@ var SessionStoreInternal = {
this._openWindowWithState({ windows: [winState] });
}
}
// Merge closed windows from this session with ones from last session
if (lastSessionState._closedWindows) {
this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
this._capClosedWindows();
+ this._closedObjectsChanged = true;
}
if (lastSessionState.scratchpads) {
ScratchpadManager.restoreSession(lastSessionState.scratchpads);
}
// Set data that persists between sessions
this._recentCrashes = lastSessionState.session &&
lastSessionState.session.recentCrashes || 0;
// Update the session start time using the restored session state.
this._updateSessionStartTime(lastSessionState);
LastSession.clear();
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
},
/**
* Revive a crashed tab and restore its state from before it crashed.
*
* @param aTab
* A <xul:tab> linked to a crashed browser. This is a no-op if the
* browser hasn't actually crashed, or is not associated with a tab.
@@ -2730,16 +2794,19 @@ var SessionStoreInternal = {
this._resetLocalTabRestoringState(tab);
}
// Restore the state into the tab.
this.restoreTab(tab, tabState, options);
});
tab.linkedBrowser.__SS_restoreState = TAB_STATE_WILL_RESTORE;
+
+ // Notify of changes to closed objects.
+ this._notifyOfClosedObjectsChange();
},
/**
* Retrieves the latest session history information for a tab. The cached data
* is returned immediately, but a callback may be provided that supplies
* up-to-date data when or if it is available. The callback is passed a single
* argument with data in the same format as the return value.
*
@@ -3274,16 +3341,17 @@ var SessionStoreInternal = {
debug(ex);
this._sendRestoreCompletedNotifications();
return;
}
// Restore closed windows if any.
if (root._closedWindows) {
this._closedWindows = root._closedWindows;
+ this._closedObjectsChanged = true;
}
// We're done here if there are no windows.
if (!root.windows || !root.windows.length) {
this._sendRestoreCompletedNotifications();
return;
}
@@ -3777,16 +3845,45 @@ var SessionStoreInternal = {
}
SessionSaver.runDelayed();
},
/* ........ Auxiliary Functions .............. */
/**
+ * Remove a closed window from the list of closed windows and indicate that
+ * the change should be notified.
+ *
+ * @param index
+ * The index of the window in this._closedWindows.
+ *
+ * @returns Array of closed windows.
+ */
+ _removeClosedWindow(index) {
+ let windows = this._closedWindows.splice(index, 1);
+ this._closedObjectsChanged = true;
+ return windows;
+ },
+
+ /**
+ * Notifies observers that the list of closed tabs and/or windows has changed.
+ * Waits a tick to allow SessionStorage a chance to register the change.
+ */
+ _notifyOfClosedObjectsChange() {
+ if (!this._closedObjectsChanged) {
+ return;
+ }
+ this._closedObjectsChanged = false;
+ setTimeout(() => {
+ Services.obs.notifyObservers(null, NOTIFY_CLOSED_OBJECTS_CHANGED, null);
+ }, 0);
+ },
+
+ /**
* Determines whether or not a tab that is being restored needs
* to have its remoteness flipped first.
*
* @param (object) with the following properties:
*
* tabbrowser (<xul:tabbrowser>):
* The tabbrowser that the browser belongs to.
*
@@ -4338,17 +4435,20 @@ var SessionStoreInternal = {
let normalWindowIndex = 0;
// try to find a non-popup window in this._closedWindows
while (normalWindowIndex < this._closedWindows.length &&
!!this._closedWindows[normalWindowIndex].isPopup)
normalWindowIndex++;
if (normalWindowIndex >= this._max_windows_undo)
spliceTo = normalWindowIndex + 1;
}
- this._closedWindows.splice(spliceTo, this._closedWindows.length);
+ if (spliceTo < this._closedWindows.length) {
+ this._closedWindows.splice(spliceTo, this._closedWindows.length);
+ this._closedObjectsChanged = true;
+ }
},
/**
* Clears the set of windows that are "resurrected" before writing to disk to
* make closing windows one after the other until shutdown work as expected.
*
* This function should only be called when we are sure that there has been
* a user action that indicates the browser is actively being used and all