Bug 1465480 - Add ContentDelegate.onCrash() r=jchen,droeh draft
authorJames Willcox <snorp@snorp.net>
Wed, 30 May 2018 13:10:54 -0500
changeset 804891 28cb36003be4862468c891365f6941b33ee26ac8
parent 804800 c97071471712a02b8fb251cd12b8002d5fc11b55
child 804892 a4091f3021053e2c992959a0b35b26f1c34fd339
push id112488
push userbmo:snorp@snorp.net
push dateWed, 06 Jun 2018 17:59:13 +0000
reviewersjchen, droeh
bugs1465480
milestone62.0a1
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,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
+        });
+      }
+    }
+  }
 }