# HG changeset patch # User Mike Conley # Date 1413324788 14400 # Tue Oct 14 18:13:08 2014 -0400 # Node ID 99eeb5c593839f63c7021bc1f75a5ef426586a9a # Parent 0dbb2983fbddcddb0ca22c95134bf0f9e970e775 Bug 1065785 - [e10s] Use SessionStore to reload crashed tabs. r=? diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2556,18 +2556,18 @@ let BrowserOnClick = { let button = event.originalTarget; if (button.id == "tryAgain") { let browser = gBrowser.getBrowserForDocument(ownerDoc); #ifdef MOZ_CRASHREPORTER if (ownerDoc.getElementById("checkSendReport").checked) { TabCrashReporter.submitCrashReport(browser); } #endif - - TabCrashReporter.reloadCrashedTab(browser); + let tab = gBrowser.getTabForBrowser(browser); + SessionStore.reviveCrashedTab(tab); } }, ignoreWarningButton: function (isMalware) { // Allow users to override and continue through to the site, // but add a notify bar as a reminder, so that they don't lose // track after, e.g., tab switching. gBrowser.loadURIWithFlags(gBrowser.currentURI.spec, diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -273,16 +273,20 @@ this.SessionStore = { restoreLastSession: function ss_restoreLastSession() { SessionStoreInternal.restoreLastSession(); }, getCurrentState: function (aUpdateAll) { return SessionStoreInternal.getCurrentState(aUpdateAll); }, + reviveCrashedTab(aTab) { + return SessionStoreInternal.reviveCrashedTab(aTab); + }, + /** * Backstage pass to implementation details, used for testing purpose. * Controlled by preference "browser.sessionstore.testmode". */ get _internal() { if (Services.prefs.getBoolPref("browser.sessionstore.debug")) { return SessionStoreInternal; } @@ -1947,16 +1951,45 @@ let SessionStoreInternal = { // Update the session start time using the restored session state. this._updateSessionStartTime(lastSessionState); LastSession.clear(); }, /** + * Revive a crashed tab and restore its state from before it crashed. + * + * @param aTab + * A linked to a crashed browser. This is a no-op if the + * browser hasn't actually crashed, or is not associated with a tab. + * This function will also throw if the browser happens to be remote. + */ + reviveCrashedTab(aTab) { + if (!aTab) { + throw new Error("SessionStore.reviveCrashedTab expected a tab, but got null."); + } + + let browser = aTab.linkedBrowser; + if (!this._crashedBrowsers.has(browser)) { + return; + } + + // Sanity check - the browser to be revived should not be remote + // at this point. + if (browser.isRemoteBrowser) { + throw new Error("SessionStore.reviveCrashedTab: " + + "Somehow a crashed browser is still remote.") + } + + let data = TabState.collect(aTab); + this.restoreTab(aTab, data); + }, + + /** * See if aWindow is usable for use when restoring a previous session via * restoreLastSession. If usable, prepare it for use. * * @param aWindow * the window to inspect & prepare * @returns [canUseWindow, canOverwriteTabs] * canUseWindow: can the window be used to restore into * canOverwriteTabs: all of the current tabs are home pages and we diff --git a/browser/components/sessionstore/test/browser_crashedTabs.js b/browser/components/sessionstore/test/browser_crashedTabs.js --- a/browser/components/sessionstore/test/browser_crashedTabs.js +++ b/browser/components/sessionstore/test/browser_crashedTabs.js @@ -2,44 +2,42 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const PAGE_1 = "data:text/html,A%20regular,%20everyday,%20normal%20page."; const PAGE_2 = "data:text/html,Another%20regular,%20everyday,%20normal%20page."; /** - * This frame script is injected into the remote browser, and used to - * intentionally crash the tab. We crash by using js-ctypes and dereferencing - * a bad pointer. The crash should happen immediately upon loading this - * frame script. - */ -function frame_script() { - const Cu = Components.utils; - Cu.import("resource://gre/modules/ctypes.jsm"); - - let dies = function() { - let zero = new ctypes.intptr_t(8); - let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); - badptr.contents - }; - - dump("Et tu, Brute?"); - dies(); -} - -/** * Returns a Promise that resolves once a remote has experienced * a crash. Also does the job of cleaning up the minidump of the crash. * * @param browser * The that will crash * @return Promise */ function crashBrowser(browser) { + // This frame script is injected into the remote browser, and used to + // intentionally crash the tab. We crash by using js-ctypes and dereferencing + // a bad pointer. The crash should happen immediately upon loading this + // frame script. + let frame_script = () => { + const Cu = Components.utils; + Cu.import("resource://gre/modules/ctypes.jsm"); + + let dies = function() { + let zero = new ctypes.intptr_t(8); + let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); + badptr.contents + }; + + dump("Et tu, Brute?"); + dies(); + } + let crashCleanupPromise = new Promise((resolve, reject) => { let observer = (subject, topic, data) => { is(topic, 'ipc:content-shutdown', 'Received correct observer topic.'); ok(subject instanceof Ci.nsIPropertyBag2, 'Subject implements nsIPropertyBag2.'); // we might see this called as the process terminates due to previous tests. // We are only looking for "abnormal" exits... if (!subject.hasKey("abnormal")) { @@ -104,16 +102,87 @@ function removeFile(directory, filename) let file = directory.clone(); file.append(filename); if (file.exists()) { file.remove(false); } } /** + * Checks the documentURI of the root document of a remote browser + * to see if it equals URI. Returns a Promise that resolves if + * there is a match, and rejects with an error message if they + * do not match. + * + * @param browser + * The remote to check the root document URI in. + * @param URI + * A string to match the root document URI against. + * @return Promise + */ +function promiseContentDocumentURIEquals(browser, URI) { + return new Promise((resolve, reject) => { + let frame_script = () => { + sendAsyncMessage("test:documenturi", { + uri: content.document.documentURI, + }); + }; + + let mm = browser.messageManager; + mm.addMessageListener("test:documenturi", function onMessage(message) { + mm.removeMessageListener("test:documenturi", onMessage); + let contentURI = message.data.uri; + if (contentURI == URI) { + resolve(); + } else { + reject(`Content has URI ${contentURI} which does not match ${URI}`); + } + }); + + mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false); + }); +} + +/** + * Checks the window.history.length of the root window of a remote + * browser to see if it equals length. Returns a Promise that resolves + * if there is a match, and rejects with an error message if they + * do not match. + * + * @param browser + * The remote to check the root window.history.length + * @param length + * The expected history length + * @return Promise + */ +function promiseHistoryLength(browser, length) { + return new Promise((resolve, reject) => { + let frame_script = () => { + sendAsyncMessage("test:historylength", { + length: content.history.length, + }); + }; + + let mm = browser.messageManager; + mm.addMessageListener("test:historylength", function onMessage(message) { + mm.removeMessageListener("test:historylength", onMessage); + let contentLength = message.data.length; + if (contentLength == length) { + resolve(); + } else { + reject(`Content has window.history.length ${contentLength} which does ` + + `not equal expected ${length}`); + } + }); + + mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false); + }); +} + +/** * Checks that if a tab crashes, that information about the tab crashed * page does not get added to the tab history. */ add_task(function test_crash_page_not_in_history() { let newTab = gBrowser.addTab(); gBrowser.selectedTab = newTab; let browser = newTab.linkedBrowser; ok(browser.isRemoteBrowser, "Should be a remote browser"); @@ -206,8 +275,46 @@ add_task(function test_revived_history_f is(entries.length, 2, "Should have two history entries"); is(entries[0].url, PAGE_1, "First entry should be the page we visited before crashing"); is(entries[1].url, "about:mozilla", "Second entry should be the page we visited after crashing"); gBrowser.removeTab(newTab); }); + +/** + * Checks that we can revive a crashed tab back to the page that + * it was on when it crashed. + */ +add_task(function test_revive_tab_from_session_store() { + let newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + let browser = newTab.linkedBrowser; + ok(browser.isRemoteBrowser, "Should be a remote browser"); + yield promiseBrowserLoaded(browser); + + browser.loadURI(PAGE_1); + yield promiseBrowserLoaded(browser); + + browser.loadURI(PAGE_2); + yield promiseBrowserLoaded(browser); + + TabState.flush(browser); + + // Crash the tab + yield crashBrowser(browser); + + // Use SessionStore to revive the tab + SessionStore.reviveCrashedTab(newTab); + yield promiseBrowserLoaded(browser); + + // We can't just check browser.currentURI.spec, because from + // the outside, a crashed tab has the same URI as the page + // it crashed on (much like an about:neterror page). Instead, + // we have to use the documentURI on the content. + yield promiseContentDocumentURIEquals(browser, PAGE_2); + + // We should also have two entries in the browser history. + yield promiseHistoryLength(browser, 2); + + gBrowser.removeTab(newTab); +}); \ No newline at end of file diff --git a/browser/modules/TabCrashReporter.jsm b/browser/modules/TabCrashReporter.jsm --- a/browser/modules/TabCrashReporter.jsm +++ b/browser/modules/TabCrashReporter.jsm @@ -82,28 +82,16 @@ this.TabCrashReporter = { if (this.browserMap.get(browser) == childID) { this.browserMap.delete(browser); browser.contentDocument.documentElement.classList.remove("crashDumpAvailable"); } } } }, - reloadCrashedTab: function (browser) { - if (browser.isRemoteBrowser) - return; - - let doc = browser.contentDocument; - if (!doc.documentURI.startsWith("about:tabcrashed")) - return; - - let url = browser.currentURI.spec; - browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null); - }, - onAboutTabCrashedLoad: function (aBrowser) { if (!this.childMap) return; let dumpID = this.childMap.get(this.browserMap.get(aBrowser)); if (!dumpID) return;