Bug 1557096 - Add ContentDelegate.onKill() to differentiate between content process crashes and kills. r=agi,snorp a=RyanVM
authorAlvina Waseem <awaseem@mozilla.com>
Thu, 01 Aug 2019 19:13:32 -0700
changeset 544892 d1491125be2f70553233acc8113eb9340496122e
parent 544891 43c35ee16e3ebeeb54b99f2d2467b30e22ba69ef
child 544893 d00a9af203d160c2584500ad7c52a27658142342
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersagi, snorp, RyanVM
bugs1557096
milestone69.0
Bug 1557096 - Add ContentDelegate.onKill() to differentiate between content process crashes and kills. r=agi,snorp a=RyanVM Differential Revision: https://phabricator.services.mozilla.com/D39732
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/RuntimeCreator.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/modules/geckoview/ContentCrashHandler.jsm
mobile/android/modules/geckoview/GeckoViewContent.jsm
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -460,16 +460,17 @@ package org.mozilla.geckoview {
   public static interface GeckoSession.ContentDelegate {
     method @UiThread default public void onCloseRequest(@NonNull GeckoSession);
     method @UiThread default public void onContextMenu(@NonNull GeckoSession, int, int, @NonNull GeckoSession.ContentDelegate.ContextElement);
     method @UiThread default public void onCrash(@NonNull GeckoSession);
     method @UiThread default public void onExternalResponse(@NonNull GeckoSession, @NonNull GeckoSession.WebResponseInfo);
     method @UiThread default public void onFirstComposite(@NonNull GeckoSession);
     method @UiThread default public void onFocusRequest(@NonNull GeckoSession);
     method @UiThread default public void onFullScreen(@NonNull GeckoSession, boolean);
+    method @UiThread default public void onKill(@NonNull GeckoSession);
     method @UiThread default public void onTitleChange(@NonNull GeckoSession, @Nullable String);
     method @UiThread default public void onWebAppManifest(@NonNull GeckoSession, @NonNull JSONObject);
   }
 
   public static class GeckoSession.ContentDelegate.ContextElement {
     ctor protected ContextElement(@Nullable String, @Nullable String, @Nullable String, @Nullable String, @NonNull String, @Nullable String);
     field public static final int TYPE_AUDIO = 3;
     field public static final int TYPE_IMAGE = 1;
--- 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
@@ -1,31 +1,36 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
+import android.app.ActivityManager
+import android.content.Context
 import android.app.assist.AssistStructure
 import android.graphics.SurfaceTexture
 import android.net.Uri
 import android.os.Build
+import android.os.Process
+import org.mozilla.gecko.GeckoAppShell
 import org.mozilla.geckoview.AllowOrDeny
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest
 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.rule.GeckoSessionTestRule.WithDevToolsAPI
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
 import org.mozilla.geckoview.test.util.UiThreadUtils
 
 import android.os.Looper
+import android.support.annotation.AnyThread
 import android.support.test.InstrumentationRegistry
 import android.support.test.filters.MediumTest
 import android.support.test.filters.SdkSuppress
 import android.support.test.runner.AndroidJUnit4
 import android.text.InputType
 import android.util.SparseArray
 import android.view.Surface
 import android.view.View
@@ -102,17 +107,17 @@ class ContentDelegateTest : BaseSessionT
 
         mainSession.loadUri(CONTENT_CRASH_URL)
         mainSession.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
         mainSession.open()
         mainSession.loadTestPath(HELLO_HTML_PATH)
         mainSession.waitUntilCalled(object: Callbacks.ProgressDelegate {
             @AssertCalled(count = 1)
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 assertThat("Page should load successfully", success, equalTo(true))
@@ -163,19 +168,85 @@ class ContentDelegateTest : BaseSessionT
         val newSession = sessionRule.createOpenSession()
         mainSession.loadUri(CONTENT_CRASH_URL)
 
         // We can inadvertently catch the `onCrash` call for the cached session if we don't specify
         // individual sessions here. Therefore, assert 'onCrash' is called for the two sessions
         // individually.
         val remainingSessions = mutableListOf(newSession, mainSession)
         while (remainingSessions.isNotEmpty()) {
+            val onCrashCalled = GeckoResult<Void>()
+            sessionRule.delegateDuringNextWait(object : Callbacks.ContentDelegate {
+                // Slower devices may not catch crashes in a timely manner, so we check to see
+                // if either `onKill` or `onCrash` is called
+                override fun onCrash(session: GeckoSession) {
+                    remainingSessions.remove(session)
+                    onCrashCalled.complete(null)
+                }
+                override fun onKill(session: GeckoSession) {
+                    remainingSessions.remove(session)
+                    onCrashCalled.complete(null)
+                }
+            })
+            sessionRule.waitForResult(onCrashCalled)
+        }
+    }
+
+    @AnyThread
+    fun killContentProcess() {
+        val context = GeckoAppShell.getApplicationContext()
+        val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+        for (info in manager.runningAppProcesses) {
+            if (info.processName.endsWith(":tab")) {
+                Process.killProcess(info.pid)
+            }
+        }
+    }
+
+    @IgnoreCrash
+    @ReuseSession(false)
+    @Test fun killContent() {
+        assumeThat(sessionRule.env.isMultiprocess, equalTo(true))
+        assumeThat(sessionRule.env.isDebugBuild && sessionRule.env.isX86,
+                equalTo(false))
+
+        killContentProcess()
+        mainSession.waitUntilCalled(object : Callbacks.ContentDelegate {
+            @AssertCalled(count = 1)
+            override fun onKill(session: GeckoSession) {
+                assertThat("Session should be closed after being killed",
+                        session.isOpen, equalTo(false))
+            }
+        })
+
+        mainSession.open()
+        mainSession.loadTestPath(HELLO_HTML_PATH)
+        mainSession.waitUntilCalled(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 killContentMultipleSessions() {
+        assumeThat(sessionRule.env.isMultiprocess, equalTo(true))
+        assumeThat(sessionRule.env.isDebugBuild && sessionRule.env.isX86,
+                equalTo(false))
+
+        val newSession = sessionRule.createOpenSession()
+        killContentProcess()
+
+        val remainingSessions = mutableListOf(newSession, mainSession)
+        while (remainingSessions.isNotEmpty()) {
             sessionRule.waitUntilCalled(object : Callbacks.ContentDelegate {
                 @AssertCalled(count = 1)
-                override fun onCrash(session: GeckoSession) {
+                override fun onKill(session: GeckoSession) {
                     remainingSessions.remove(session)
                 }
             })
         }
     }
 
     val ViewNode by lazy {
         AssistStructure.ViewNode::class.java.getDeclaredConstructor().apply { isAccessible = true }
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
@@ -95,17 +95,17 @@ public class GeckoSessionTestRule implem
         try {
             sOnPageStart = GeckoSession.ProgressDelegate.class.getMethod(
                     "onPageStart", GeckoSession.class, String.class);
             sOnPageStop = GeckoSession.ProgressDelegate.class.getMethod(
                     "onPageStop", GeckoSession.class, boolean.class);
             sOnNewSession = GeckoSession.NavigationDelegate.class.getMethod(
                     "onNewSession", GeckoSession.class, String.class);
             sOnCrash = GeckoSession.ContentDelegate.class.getMethod(
-                    "onCrash", GeckoSession.class);
+                    "onKill", GeckoSession.class);
         } catch (final NoSuchMethodException e) {
             throw new RuntimeException(e);
         }
     }
 
     /**
      * Specify the timeout for any of the wait methods, in milliseconds, relative to
      * {@link Environment#DEFAULT_TIMEOUT_MILLIS}. When the default timeout scales to account
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/RuntimeCreator.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/RuntimeCreator.java
@@ -15,21 +15,18 @@ public class RuntimeCreator {
             return sRuntime;
         }
 
         final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
                 new GeckoRuntimeSettings.Builder();
         runtimeSettingsBuilder.arguments(new String[]{"-purgecaches"})
                 .extras(InstrumentationRegistry.getArguments())
                 .remoteDebuggingEnabled(true)
-                .consoleOutput(true);
-
-        if (new Environment().isAutomation()) {
-            runtimeSettingsBuilder.crashHandler(TestCrashHandler.class);
-        }
+                .consoleOutput(true)
+                .crashHandler(TestCrashHandler.class);
 
         sRuntime = GeckoRuntime.create(
                 InstrumentationRegistry.getTargetContext(),
                 runtimeSettingsBuilder.build());
 
         sRuntime.setDelegate(new GeckoRuntime.Delegate() {
             @Override
             public void onShutdown() {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
@@ -183,17 +183,17 @@ public final class GeckoRuntime implemen
         @Override
         public void handleMessage(final String event, final GeckoBundle message,
                                   final EventCallback callback) {
             final Class<?> crashHandler = GeckoRuntime.this.getSettings().mCrashHandler;
 
             if ("Gecko:Exited".equals(event) && mDelegate != null) {
                 mDelegate.onShutdown();
                 EventDispatcher.getInstance().unregisterUiThreadListener(mEventListener, "Gecko:Exited");
-            } else if ("GeckoView:ContentCrash".equals(event) && crashHandler != null) {
+            } else if ("GeckoView:ContentCrashReport".equals(event) && crashHandler != null) {
                 final Context context = GeckoAppShell.getApplicationContext();
                 Intent i = new Intent(ACTION_CRASHED, null,
                         context, crashHandler);
                 i.putExtra(EXTRA_MINIDUMP_PATH, message.getString(EXTRA_MINIDUMP_PATH));
                 i.putExtra(EXTRA_EXTRAS_PATH, message.getString(EXTRA_EXTRAS_PATH));
                 i.putExtra(EXTRA_MINIDUMP_SUCCESS, true);
                 i.putExtra(EXTRA_CRASH_FATAL, message.getBoolean(EXTRA_CRASH_FATAL, true));
 
@@ -233,17 +233,17 @@ public final class GeckoRuntime implemen
         final Class<?> crashHandler = settings.getCrashHandler();
         if (crashHandler != null) {
             try {
                 final ServiceInfo info = context.getPackageManager().getServiceInfo(new ComponentName(context, crashHandler), 0);
                 if (info.processName.equals(getProcessName(context))) {
                     throw new IllegalArgumentException("Crash handler service must run in a separate process");
                 }
 
-                EventDispatcher.getInstance().registerUiThreadListener(mEventListener, "GeckoView:ContentCrash");
+                EventDispatcher.getInstance().registerUiThreadListener(mEventListener, "GeckoView:ContentCrashReport");
 
                 flags |= GeckoThread.FLAG_ENABLE_NATIVE_CRASHREPORTER;
             } catch (PackageManager.NameNotFoundException e) {
                 throw new IllegalArgumentException("Crash handler must be registered as a service");
             }
         }
 
         GeckoAppShell.useMaxScreenDepth(settings.getUseMaxScreenDepth());
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -457,35 +457,38 @@ public class GeckoSession implements Par
         mWebExtensionListener.setDelegate(webExtension, delegate, nativeApp);
     }
 
     private final GeckoSessionHandler<ContentDelegate> mContentHandler =
         new GeckoSessionHandler<ContentDelegate>(
             "GeckoViewContent", this,
             new String[]{
                 "GeckoView:ContentCrash",
+                "GeckoView:ContentKill",
                 "GeckoView:ContextMenu",
                 "GeckoView:DOMTitleChanged",
                 "GeckoView:DOMWindowClose",
                 "GeckoView:ExternalResponse",
                 "GeckoView:FocusRequest",
                 "GeckoView:FullScreenEnter",
                 "GeckoView:FullScreenExit",
                 "GeckoView:WebAppManifest",
             }
         ) {
             @Override
             public void handleMessage(final ContentDelegate delegate,
                                       final String event,
                                       final GeckoBundle message,
                                       final EventCallback callback) {
-
                 if ("GeckoView:ContentCrash".equals(event)) {
                     close();
                     delegate.onCrash(GeckoSession.this);
+                } else if ("GeckoView:ContentKill".equals(event)) {
+                    close();
+                    delegate.onKill(GeckoSession.this);
                 } else if ("GeckoView:ContextMenu".equals(event)) {
                     final ContentDelegate.ContextElement elem =
                         new ContentDelegate.ContextElement(
                             message.getString("baseUri"),
                             message.getString("uri"),
                             message.getString("title"),
                             message.getString("alt"),
                             message.getString("elementType"),
@@ -3177,22 +3180,35 @@ public class GeckoSession implements Par
 
         /**
          * 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.
+         * @param session The GeckoSession for which the content process has crashed.
          */
         @UiThread
         default void onCrash(@NonNull GeckoSession session) {}
 
         /**
+         * The content process hosting this GeckoSession has been killed. 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 for which the content process has been killed.
+         */
+        @UiThread
+        default void onKill(@NonNull GeckoSession session) {}
+
+
+        /**
          * Notification that the first content composition has occurred.
          * This callback is invoked for the first content composite after either
          * a start or a restart of the compositor.
          * @param session The GeckoSession that had a first paint event.
          */
         @UiThread
         default void onFirstComposite(@NonNull GeckoSession session) {}
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -17,16 +17,19 @@ exclude: true
 
 - [`GeckoSession.setMessageDelegate`][69.2] callers must now specify the
   [`WebExtension`][69.3] that the [`MessageDelegate`][69.4] will receive
   messages from.
 
 [69.2]: ../GeckoSession.html#setMessageDelegate-org.mozilla.geckoview.WebExtension-org.mozilla.geckoview.WebExtension.MessageDelegate-java.lang.String-
 [69.3]: ../WebExtension.html
 [69.4]: ../WebExtension.MessageDelegate.html
+[69.7]: ../GeckoSession.ContentDelegate.html#onKill
+
+- Created `onKill` to `ContentDelegate` to differentiate from crashes.
 
 ## v68
 - Added [`GeckoRuntime#configurationChanged`][68.1] to notify the device
   configuration has changed.
 
 [68.1]: ../GeckoRuntime.html#configurationChanged
 
 - Added `onSessionStateChange` to [`ProgressDelegate`][68.2] and removed `saveState`.
@@ -330,9 +333,9 @@ exclude: true
 [65.23]: ../GeckoSession.FinderResult.html
 
 - Update [`CrashReporter#sendCrashReport`][65.24] to return the crash ID as a
   [`GeckoResult<String>`][65.25].
 
 [65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: ../GeckoResult.html
 
-[api-version]: d143ce8f7ed19ff68f5d48243e34aed609767c5a
+[api-version]: 7b53d1a6703cc7f613de81c422dccdd95b6153fe
--- a/mobile/android/modules/geckoview/ContentCrashHandler.jsm
+++ b/mobile/android/modules/geckoview/ContentCrashHandler.jsm
@@ -60,16 +60,16 @@ var ContentCrashHandler = {
         .add(1);
       return;
     }
 
     debug`Notifying content process crash, dump ID ${dumpID}`;
     const [minidumpPath, extrasPath] = getPendingMinidump(dumpID);
 
     EventDispatcher.instance.sendRequest({
-      type: "GeckoView:ContentCrash",
+      type: "GeckoView:ContentCrashReport",
       minidumpPath,
       extrasPath,
       success: true,
       fatal: false,
     });
   },
 };
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -55,16 +55,17 @@ class GeckoViewContent extends GeckoView
 
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenExit", this);
     this.messageManager.addMessageListener(
       "GeckoView:DOMFullscreenRequest",
       this
     );
 
     Services.obs.addObserver(this, "oop-frameloader-crashed");
+    Services.obs.addObserver(this, "ipc:content-shutdown");
   }
 
   onDisable() {
     this.window.removeEventListener(
       "MozDOMFullscreen:Entered",
       this,
       /* capture */ true
     );
@@ -84,16 +85,17 @@ class GeckoViewContent extends GeckoView
       this
     );
     this.messageManager.removeMessageListener(
       "GeckoView:DOMFullscreenRequest",
       this
     );
 
     Services.obs.removeObserver(this, "oop-frameloader-crashed");
+    Services.obs.removeObserver(this, "ipc:content-shutdown");
   }
 
   // Bundle event handler.
   onEvent(aEvent, aData, aCallback) {
     debug`onEvent: event=${aEvent}, data=${aData}`;
 
     switch (aEvent) {
       case "GeckoViewContent:ExitFullScreen":
@@ -190,27 +192,48 @@ class GeckoViewContent extends GeckoView
         this.window.windowUtils.remoteFrameFullscreenChanged(aMsg.target);
         break;
     }
   }
 
   // nsIObserver event handler
   observe(aSubject, aTopic, aData) {
     debug`observe: ${aTopic}`;
+    this._contentCrashed = false;
+    const browser = aSubject.ownerElement;
 
     switch (aTopic) {
       case "oop-frameloader-crashed": {
-        const browser = aSubject.ownerElement;
         if (!browser || browser != this.browser) {
           return;
         }
-
-        this.eventDispatcher.sendRequest({
-          type: "GeckoView:ContentCrash",
-        });
+        this.window.setTimeout(() => {
+          if (this._contentCrashed) {
+            this.eventDispatcher.sendRequest({
+              type: "GeckoView:ContentCrash",
+            });
+          } else {
+            this.eventDispatcher.sendRequest({
+              type: "GeckoView:ContentKill",
+            });
+          }
+        }, 250);
+        break;
+      }
+      case "ipc:content-shutdown": {
+        aSubject.QueryInterface(Ci.nsIPropertyBag2);
+        if (aSubject.get("dumpID")) {
+          if (
+            browser &&
+            aSubject.get("childID") != browser.frameLoader.childID
+          ) {
+            return;
+          }
+          this._contentCrashed = true;
+        }
         break;
       }
     }
   }
 
   _findInPage(aData, aCallback) {
     debug`findInPage: data=${aData} callback=${aCallback && "non-null"}`;