Bug 1557096 - Add ContentDelegate.onKill() to differentiate between content process crashes and kills. a=RyanVM
☠☠ backed out by ba09b92b6286 ☠ ☠
authorAlvina Waseem <awaseem@mozilla.com>
Thu, 25 Jul 2019 02:48:21 +0300
changeset 544720 58fc6628c2cb331ac99879b2426e712f021c9142
parent 544719 162ad0aaccf9bd329d189184577e18fea1101911
child 544721 5d678cfdd1ddbf16f300dd83057698c0b84e8880
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)
reviewersRyanVM
bugs1557096
milestone69.0
Bug 1557096 - Add ContentDelegate.onKill() to differentiate between content process crashes and kills. a=RyanVM Differential Revision: https://phabricator.services.mozilla.com//D35874
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/gecko/process/GeckoProcessManager.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/modules/geckoview/ContentCrashHandler.jsm
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -407,16 +407,17 @@ package org.mozilla.geckoview {
     method @AnyThread @NonNull public GeckoSessionSettings getSettings();
     method @UiThread public void getSurfaceBounds(@NonNull Rect);
     method @AnyThread @NonNull public SessionTextInput getTextInput();
     method @AnyThread @NonNull public GeckoResult<String> getUserAgent();
     method @AnyThread public void goBack();
     method @AnyThread public void goForward();
     method @AnyThread public void gotoHistoryIndex(int);
     method @AnyThread public boolean isOpen();
+    method public static void killContentProcess();
     method @AnyThread public void loadData(@NonNull byte[], @Nullable String);
     method @AnyThread public void loadString(@NonNull String, @Nullable String);
     method @AnyThread public void loadUri(@NonNull String);
     method @AnyThread public void loadUri(@NonNull String, int);
     method @AnyThread public void loadUri(@NonNull String, @Nullable String, int);
     method @AnyThread public void loadUri(@NonNull Uri);
     method @AnyThread public void loadUri(@NonNull Uri, int);
     method @AnyThread public void loadUri(@NonNull Uri, @Nullable Uri, int);
@@ -460,16 +461,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,46 +1,52 @@
 /* -*- 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.app.assist.AssistStructure
+import android.content.Context
 import android.graphics.SurfaceTexture
 import android.net.Uri
 import android.os.Build
+import org.mozilla.gecko.process.GeckoProcessManager
 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.os.Process
+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
 import android.view.ViewStructure
 import android.widget.EditText
 import org.hamcrest.Matchers.*
 import org.json.JSONObject
 import org.junit.Assume.assumeThat
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mozilla.gecko.GeckoAppShell
 import org.mozilla.geckoview.test.util.HttpBin
 
 import java.net.URI
 
 import kotlin.concurrent.thread
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
@@ -102,17 +108,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))
@@ -172,16 +178,74 @@ class ContentDelegateTest : BaseSessionT
                 @AssertCalled(count = 1)
                 override fun onCrash(session: GeckoSession) {
                     remainingSessions.remove(session)
                 }
             })
         }
     }
 
+    @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 onKill(session: GeckoSession) {
+                    remainingSessions.remove(session)
+                }
+            })
+        }
+    }
+
     val ViewNode by lazy {
         AssistStructure.ViewNode::class.java.getDeclaredConstructor().apply { isAccessible = true }
     }
 
     val ViewNodeBuilder by lazy {
         Class.forName("android.app.assist.AssistStructure\$ViewNodeBuilder")
                 .getDeclaredConstructor(AssistStructure::class.java,
                                         AssistStructure.ViewNode::class.java,
--- 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/gecko/process/GeckoProcessManager.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
@@ -193,24 +193,34 @@ public final class GeckoProcessManager e
             if (connection.bind() != null) {
                 connection.getPid();
             }
         }
     }
 
     public void crashChild() {
         try {
-            IChildProcess childProcess = mConnections.get("tab").bind();
+            final IChildProcess childProcess = mConnections.get("tab").bind();
             if (childProcess != null) {
                 childProcess.crash();
             }
         } catch (RemoteException e) {
         }
     }
 
+    public void killChild() {
+        try {
+            final IChildProcess childProcess = mConnections.get("tab").bind();
+            if (childProcess != null) {
+                Process.killProcess(childProcess.getPid());
+            }
+        } catch (RemoteException e) {
+        }
+    }
+
     @WrapForJNI
     private static int start(final String type, final String[] args,
                              final int prefsFd, final int prefMapFd,
                              final int ipcFd,
                              final int crashFd, final int crashAnnotationFd) {
         return INSTANCE.start(type, args, prefsFd, prefMapFd, ipcFd, crashFd, crashAnnotationFd, /* retry */ false);
     }
 
--- 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/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,
     });
   },
 };