Bug 1465480 - Add ContentDelegate.onCrash() r=jchen,droeh
authorJames Willcox <snorp@snorp.net>
Wed, 30 May 2018 13:10:54 -0500
changeset 422036 d9d0a8abad4028f967d63fa8667a7c925c6d1de9
parent 422035 951bf64cc7982ec8ea83222757bec6e291744e6f
child 422037 8814700645f0c33ad34d99bb9a56c8320ea28043
push id34114
push userbtara@mozilla.com
push dateSat, 09 Jun 2018 15:31:58 +0000
treeherdermozilla-central@e02a5155d815 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen, droeh
bugs1465480
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/modules/geckoview/GeckoViewContent.jsm
mobile/android/modules/geckoview/GeckoViewProgress.jsm
--- 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,28 @@ 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) {
+    debug `observe: ${aTopic}`;
+
+    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
+        });
+      }
+    }
+  }
 }