Bug 1465480 - Add ContentDelegate.onCrash() r=jchen,droeh
This will give applications the opportunity to recover from a content
process crash.
MozReview-Commit-ID: IAfVNy3ndS4
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -895,16 +895,22 @@ public abstract class GeckoApp extends G
@Override
public void onContextMenu(final GeckoSession session, final int screenX,
final int screenY, final String uri,
int elementType, final String elementSrc) {
}
@Override
public void onExternalResponse(final GeckoSession session, final GeckoSession.WebResponseInfo request) {
+ // Won't happen, as we don't use the GeckoView download support in Fennec
+ }
+
+ @Override
+ public void onCrash(final GeckoSession session) {
+ // Won't happen, as we don't use e10s in Fennec
}
protected void setFullScreen(final boolean fullscreen) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
onFullScreen(mLayerView.getSession(), fullscreen);
}
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -717,16 +717,22 @@ public class CustomTabsActivity extends
public void run() {
WebApps.openInFennec(validUri, CustomTabsActivity.this);
}
});
}
@Override
public void onExternalResponse(final GeckoSession session, final GeckoSession.WebResponseInfo request) {
+ // Won't happen, as we don't use the GeckoView download support in Fennec
+ }
+
+ @Override
+ public void onCrash(final GeckoSession session) {
+ // Won't happen, as we don't use e10s in Fennec
}
@Override // ActionModePresenter
public void startActionMode(final ActionMode.Callback callback) {
endActionMode();
mActionMode = startSupportActionMode(callback);
}
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -364,16 +364,22 @@ public class WebAppActivity extends AppC
return;
}
WebApps.openInFennec(validUri, WebAppActivity.this);
}
@Override // GeckoSession.ContentDelegate
public void onExternalResponse(final GeckoSession session, final GeckoSession.WebResponseInfo request) {
+ // Won't happen, as we don't use the GeckoView download support in Fennec
+ }
+
+ @Override // GeckoSession.ContentDelegate
+ public void onCrash(final GeckoSession session) {
+ // Won't happen, as we don't use e10s in Fennec
}
@Override // GeckoSession.ContentDelegate
public void onFullScreen(GeckoSession session, boolean fullScreen) {
updateFullScreenContent(fullScreen);
}
@Override
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -2,21 +2,24 @@
* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.geckoview.test
import org.mozilla.geckoview.GeckoResponse
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.IgnoreCrash
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
import org.mozilla.geckoview.test.util.Callbacks
import android.support.test.filters.MediumTest
import android.support.test.runner.AndroidJUnit4
import org.hamcrest.Matchers.*
+import org.junit.Assume.assumeThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class ContentDelegateTest : BaseSessionTest() {
@Test fun titleChange() {
@@ -52,9 +55,58 @@ class ContentDelegateTest : BaseSessionT
assertThat("Uri should start with data:", response.uri, startsWith("data:"))
assertThat("Content type should match", response.contentType, equalTo("text/plain"))
assertThat("Content length should be non-zero", response.contentLength, greaterThan(0L))
assertThat("Filename should match", response.filename, equalTo("download.txt"))
}
})
}
+ @IgnoreCrash
+ @ReuseSession(false)
+ @Test fun crashContent() {
+ // This test doesn't make sense without multiprocess
+ assumeThat(sessionRule.env.isMultiprocess, equalTo(true))
+
+ sessionRule.session.loadUri(CONTENT_CRASH_URL)
+
+ sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
+ @AssertCalled(count = 1)
+ override fun onCrash(session: GeckoSession) {
+ assertThat("Session should be closed after a crash", session.isOpen, equalTo(false))
+
+ // Recover immediately
+ session.open()
+ session.loadTestPath(HELLO_HTML_PATH)
+ }
+ });
+
+ sessionRule.waitForPageStop()
+
+ sessionRule.forCallbacksDuringWait(object: Callbacks.ProgressDelegate {
+ @AssertCalled(count = 1)
+ override fun onPageStop(session: GeckoSession, success: Boolean) {
+ assertThat("Page should load successfully", success, equalTo(true))
+ }
+ })
+ }
+
+ @IgnoreCrash
+ @ReuseSession(false)
+ @Test fun crashContentMultipleSessions() {
+ // This test doesn't make sense without multiprocess
+ assumeThat(sessionRule.env.isMultiprocess, equalTo(true))
+
+ // We need to make sure all sessions in a given content process
+ // receive onCrash(). If we add multiple content processes, this
+ // test will need fixed to ensure the test sessions go into the
+ // same one.
+ sessionRule.createOpenSession()
+ sessionRule.session.loadUri(CONTENT_CRASH_URL)
+
+ sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
+ @AssertCalled(count = 2)
+ override fun onCrash(session: GeckoSession) {
+ assertThat("Session should be closed after a crash", session.isOpen, equalTo(false))
+ }
+ });
+ }
}
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
@@ -80,16 +80,20 @@ public class TestRunnerActivity extends
@Override
public void onContextMenu(GeckoSession session, int screenX, int screenY, String uri, int elementType, String elementSrc) {
}
@Override
public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo request) {
}
+
+ @Override
+ public void onCrash(GeckoSession session) {
+ }
};
private GeckoSession createSession() {
return createSession(null);
}
private GeckoSession createSession(GeckoSessionSettings settings) {
if (settings == null) {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
@@ -32,16 +32,19 @@ class Callbacks private constructor() {
override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
}
override fun onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, uri: String, elementType: Int, elementSrc: String) {
}
override fun onExternalResponse(session: GeckoSession, response: GeckoSession.WebResponseInfo) {
}
+
+ override fun onCrash(session: GeckoSession) {
+ }
}
interface NavigationDelegate : GeckoSession.NavigationDelegate {
override fun onLocationChange(session: GeckoSession, url: String) {
}
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -99,32 +99,36 @@ public class GeckoSession extends LayerS
private String mId = UUID.randomUUID().toString().replace("-", "");
/* package */ String getId() { return mId; }
private final GeckoSessionHandler<ContentDelegate> mContentHandler =
new GeckoSessionHandler<ContentDelegate>(
"GeckoViewContent", this,
new String[]{
+ "GeckoView:ContentCrash",
"GeckoView:ContextMenu",
"GeckoView:DOMTitleChanged",
"GeckoView:DOMWindowFocus",
"GeckoView:DOMWindowClose",
"GeckoView:ExternalResponse",
"GeckoView:FullScreenEnter",
- "GeckoView:FullScreenExit"
+ "GeckoView:FullScreenExit",
}
) {
@Override
public void handleMessage(final ContentDelegate delegate,
final String event,
final GeckoBundle message,
final EventCallback callback) {
- if ("GeckoView:ContextMenu".equals(event)) {
+ if ("GeckoView:ContentCrash".equals(event)) {
+ close();
+ delegate.onCrash(GeckoSession.this);
+ } else if ("GeckoView:ContextMenu".equals(event)) {
final int type = getContentElementType(
message.getString("elementType"));
delegate.onContextMenu(GeckoSession.this,
message.getInt("screenX"),
message.getInt("screenY"),
message.getString("uri"),
type,
@@ -1886,16 +1890,27 @@ public class GeckoSession extends LayerS
/**
* This is fired when there is a response that cannot be handled
* by Gecko (e.g., a download).
*
* @param session the GeckoSession that received the external response.
* @param response the WebResponseInfo for the external response
*/
void onExternalResponse(GeckoSession session, WebResponseInfo response);
+
+ /**
+ * The content process hosting this GeckoSession has crashed. The
+ * GeckoSession is now closed and unusable. You may call
+ * {@link #open(GeckoRuntime)} to recover the session, but no state
+ * is preserved. Most applications will want to call
+ * {@link #loadUri(Uri)} or {@link #restoreState(SessionState)} at this point.
+ *
+ * @param session The GeckoSession that crashed.
+ */
+ void onCrash(GeckoSession session);
}
public interface SelectionActionDelegate {
@IntDef(flag = true, value = {FLAG_IS_COLLAPSED,
FLAG_IS_EDITABLE})
@interface Flag {}
/**
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -204,16 +204,23 @@ public class GeckoViewActivity extends A
" screenY=" + screenY + " uri=" + uri +
" elementType=" + elementType +
" elementSrc=" + elementSrc);
}
@Override
public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo request) {
}
+
+ @Override
+ public void onCrash(GeckoSession session) {
+ Log.e(LOGTAG, "Crashed, reopening session");
+ session.open(sGeckoRuntime);
+ session.loadUri(DEFAULT_URL);
+ }
}
private class MyGeckoViewProgress implements GeckoSession.ProgressDelegate {
private MyTrackingProtection mTp;
private MyGeckoViewProgress(final MyTrackingProtection tp) {
mTp = tp;
}
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -4,16 +4,20 @@
"use strict";
var EXPORTED_SYMBOLS = ["GeckoViewContent"];
ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+});
+
class GeckoViewContent extends GeckoViewModule {
onInit() {
this.registerListener([
"GeckoViewContent:ExitFullScreen",
"GeckoView:RestoreState",
"GeckoView:SaveState",
"GeckoView:SetActive",
"GeckoView:ZoomToInput",
@@ -25,26 +29,30 @@ class GeckoViewContent extends GeckoView
onEnable() {
this.window.addEventListener("MozDOMFullscreen:Entered", this,
/* capture */ true, /* untrusted */ false);
this.window.addEventListener("MozDOMFullscreen:Exited", this,
/* capture */ true, /* untrusted */ false);
this.messageManager.addMessageListener("GeckoView:DOMFullscreenExit", this);
this.messageManager.addMessageListener("GeckoView:DOMFullscreenRequest", this);
+
+ Services.obs.addObserver(this, "oop-frameloader-crashed");
}
onDisable() {
this.window.removeEventListener("MozDOMFullscreen:Entered", this,
/* capture */ true);
this.window.removeEventListener("MozDOMFullscreen:Exited", this,
/* capture */ true);
this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExit", this);
this.messageManager.removeMessageListener("GeckoView:DOMFullscreenRequest", this);
+
+ Services.obs.removeObserver(this, "oop-frameloader-crashed");
}
// Bundle event handler.
onEvent(aEvent, aData, aCallback) {
debug `onEvent: event=${aEvent}, data=${aData}`;
switch (aEvent) {
case "GeckoViewContent:ExitFullScreen":
@@ -116,9 +124,26 @@ class GeckoViewContent extends GeckoView
warn `Failed to save state due to missing callback`;
return;
}
this._saveStateCallbacks.get(aMsg.data.id).onSuccess(aMsg.data.state);
this._saveStateCallbacks.delete(aMsg.data.id);
break;
}
}
+
+ // nsIObserver event handler
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "oop-frameloader-crashed": {
+ const browser = aSubject.ownerElement;
+ if (!browser || browser != this.browser) {
+ return;
+ }
+
+ this.eventDispatcher.sendRequest({
+ type: "GeckoView:ContentCrash"
+ });
+ }
+ break;
+ }
+ }
}
--- a/mobile/android/modules/geckoview/GeckoViewProgress.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewProgress.jsm
@@ -189,25 +189,28 @@ class GeckoViewProgress extends GeckoVie
let flags = Ci.nsIWebProgress.NOTIFY_STATE_NETWORK |
Ci.nsIWebProgress.NOTIFY_SECURITY |
Ci.nsIWebProgress.NOTIFY_LOCATION;
this.progressFilter =
Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
.createInstance(Ci.nsIWebProgress);
this.progressFilter.addProgressListener(this, flags);
this.browser.addProgressListener(this.progressFilter, flags);
+ Services.obs.addObserver(this, "oop-frameloader-crashed");
}
onDisable() {
debug `onDisable`;
if (this.progressFilter) {
this.progressFilter.removeProgressListener(this);
this.browser.removeProgressListener(this.progressFilter);
}
+
+ Services.obs.removeObserver(this, "oop-frameloader-crashed");
}
onSettingsUpdate() {
const settings = this.settings;
debug `onSettingsUpdate: ${settings}`;
IdentityHandler.setUseTrackingProtection(!!settings.useTrackingProtection);
IdentityHandler.setUsePrivateMode(!!settings.usePrivateMode);
@@ -220,24 +223,26 @@ class GeckoViewProgress extends GeckoVie
if (!aWebProgress.isTopLevel) {
return;
}
const uriSpec = aRequest.QueryInterface(Ci.nsIChannel).URI.displaySpec;
debug `onStateChange: uri=${uriSpec}`;
if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ this._inProgress = true;
const message = {
type: "GeckoView:PageStart",
uri: uriSpec,
};
this.eventDispatcher.sendRequest(message);
} else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
!aWebProgress.isLoadingDocument) {
+ this._inProgress = false;
let message = {
type: "GeckoView:PageStop",
success: !aStatus
};
this.eventDispatcher.sendRequest(message);
}
}
@@ -271,9 +276,28 @@ class GeckoViewProgress extends GeckoVie
if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
// We apparently don't get a STATE_STOP in onStateChange(), so emit PageStop here
this.eventDispatcher.sendRequest({
type: "GeckoView:PageStop",
success: false
});
}
}
+
+ // nsIObserver event handler
+ observe(aSubject, aTopic, aData) {
+ debug `observe: topic=${aTopic}`;
+
+ switch (aTopic) {
+ case "oop-frameloader-crashed": {
+ const browser = aSubject.ownerElement;
+ if (!browser || browser != this.browser || !this._inProgress) {
+ return;
+ }
+
+ this.eventDispatcher.sendRequest({
+ type: "GeckoView:PageStop",
+ success: false
+ });
+ }
+ }
+ }
}