chromium/content/public/android/javatests/src/org/chromium/content/browser/ChildProcessLauncherIntegrationTest.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;

import androidx.test.filters.MediumTest;

import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.process_launcher.ChildConnectionAllocator;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer;
import org.chromium.content_shell_apk.ChildProcessLauncherTestUtils;
import org.chromium.content_shell_apk.ContentShellActivity;
import org.chromium.content_shell_apk.ContentShellActivityTestRule;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * Integration test that starts the full shell and load pages to test ChildProcessLauncher
 * and related code.
 */
@RunWith(BaseJUnit4ClassRunner.class)
public class ChildProcessLauncherIntegrationTest {
    @Rule
    public final ContentShellActivityTestRule mActivityTestRule =
            new ContentShellActivityTestRule();

    private static class TestChildProcessConnectionFactory
            implements ChildConnectionAllocator.ConnectionFactory {
        private final List<TestChildProcessConnection> mConnections = new ArrayList<>();

        @Override
        public ChildProcessConnection createConnection(
                Context context,
                ComponentName serviceName,
                ComponentName fallbackServiceName,
                boolean bindToCaller,
                boolean bindAsExternalService,
                Bundle serviceBundle,
                String instanceName) {
            TestChildProcessConnection connection =
                    new TestChildProcessConnection(
                            context,
                            serviceName,
                            bindToCaller,
                            bindAsExternalService,
                            serviceBundle);
            mConnections.add(connection);
            return connection;
        }

        public List<TestChildProcessConnection> getConnections() {
            return mConnections;
        }
    }

    private static class TestChildProcessConnection extends ChildProcessConnection {
        private RuntimeException mRemovedBothVisibleAndStrongBinding;

        public TestChildProcessConnection(
                Context context,
                ComponentName serviceName,
                boolean bindToCaller,
                boolean bindAsExternalService,
                Bundle childProcessCommonParameters) {
            super(
                    context,
                    serviceName,
                    /* fallbackServiceName= */ null,
                    bindToCaller,
                    bindAsExternalService,
                    childProcessCommonParameters,
                    /* instanceName= */ null);
        }

        @Override
        protected void unbind() {
            super.unbind();
            if (mRemovedBothVisibleAndStrongBinding == null) {
                mRemovedBothVisibleAndStrongBinding = new RuntimeException("unbind");
            }
        }

        @Override
        public void removeVisibleBinding() {
            super.removeVisibleBinding();
            if (mRemovedBothVisibleAndStrongBinding == null && !isStrongBindingBound()) {
                mRemovedBothVisibleAndStrongBinding = new RuntimeException("removeVisibleBinding");
            }
        }

        @Override
        public void removeStrongBinding() {
            super.removeStrongBinding();
            if (mRemovedBothVisibleAndStrongBinding == null && !isVisibleBindingBound()) {
                mRemovedBothVisibleAndStrongBinding = new RuntimeException("removeStrongBinding");
            }
        }

        public void throwIfDroppedBothVisibleAndStrongBinding() {
            if (mRemovedBothVisibleAndStrongBinding != null) {
                throw new RuntimeException(mRemovedBothVisibleAndStrongBinding);
            }
        }
    }

    @Test
    @MediumTest
    // This test may run with --site-per-process or AndroidWarmUpSpareRendererWithTimeout, which
    // also enables a feature to maintain a spare renderer process.
    // The spare process interferes with assertions on the number of process connections in this
    // test, so disable it.
    @CommandLineFlags.Add({
        "disable-features=SpareRendererForSitePerProcess,AndroidWarmUpSpareRendererWithTimeout"
    })
    public void testCrossDomainNavigationDoNotLoseImportance() throws Throwable {
        final TestChildProcessConnectionFactory factory = new TestChildProcessConnectionFactory();
        final List<TestChildProcessConnection> connections = factory.getConnections();
        ChildProcessLauncherHelperImpl.setSandboxServicesSettingsForTesting(
                factory,
                10 /* arbitrary number, only really need 2 */,
                null /* use default service name */);

        // TODO(boliu,nasko): Ensure navigation is actually successful
        // before proceeding.
        ContentShellActivity activity =
                mActivityTestRule.launchContentShellWithUrlSync(
                        "content/test/data/android/title1.html");
        NavigationController navigationController =
                mActivityTestRule.getWebContents().getNavigationController();
        TestCallbackHelperContainer testCallbackHelperContainer =
                new TestCallbackHelperContainer(activity.getActiveWebContents());

        mActivityTestRule.loadUrl(
                navigationController,
                testCallbackHelperContainer,
                new LoadUrlParams(
                        UrlUtils.getIsolatedTestFileUrl(
                                "content/test/data/android/geolocation.html")));
        ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
                new Runnable() {
                    @Override
                    public void run() {
                        Assert.assertEquals(1, connections.size());
                        connections.get(0).throwIfDroppedBothVisibleAndStrongBinding();
                    }
                });

        mActivityTestRule.loadUrl(
                navigationController, testCallbackHelperContainer, new LoadUrlParams("data:,foo"));
        ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
                new Runnable() {
                    @Override
                    public void run() {
                        if (ContentFeatureMap.isEnabled(
                                ContentFeatureList.PROCESS_SHARING_WITH_STRICT_SITE_INSTANCES)) {
                            // If this feature is turned on all the URLs will use the same process.
                            // Verify that the process has not lost its importance now that the
                            // data: URL is also in the same process as the file: URLs.
                            Assert.assertEquals(1, connections.size());
                            connections.get(0).throwIfDroppedBothVisibleAndStrongBinding();
                        } else {
                            Assert.assertEquals(2, connections.size());
                            connections.get(1).throwIfDroppedBothVisibleAndStrongBinding();
                        }
                    }
                });
    }

    @Test
    @MediumTest
    // This test may run with --site-per-process or AndroidWarmUpSpareRendererWithTimeout, which
    // also enables a feature to maintain a spare renderer process.
    // The spare process interferes with assertions on the number of process connections in this
    // test, so disable it.
    @CommandLineFlags.Add({
        "disable-features=SpareRendererForSitePerProcess,AndroidWarmUpSpareRendererWithTimeout"
    })
    public void testIntentionalKillToFreeServiceSlot() throws Throwable {
        final TestChildProcessConnectionFactory factory = new TestChildProcessConnectionFactory();
        final List<TestChildProcessConnection> connections = factory.getConnections();
        ChildProcessLauncherHelperImpl.setSandboxServicesSettingsForTesting(
                factory, 1, null /* use default service name */);
        // Doing a cross-domain navigation would need to kill the first process in order to create
        // the second process.

        ContentShellActivity activity =
                mActivityTestRule.launchContentShellWithUrlSync(
                        "content/test/data/android/vsync.html");
        NavigationController navigationController =
                mActivityTestRule.getWebContents().getNavigationController();
        TestCallbackHelperContainer testCallbackHelperContainer =
                new TestCallbackHelperContainer(activity.getActiveWebContents());

        mActivityTestRule.loadUrl(
                navigationController,
                testCallbackHelperContainer,
                new LoadUrlParams(
                        UrlUtils.getIsolatedTestFileUrl(
                                "content/test/data/android/geolocation.html")));
        mActivityTestRule.loadUrl(
                navigationController, testCallbackHelperContainer, new LoadUrlParams("data:,foo"));

        ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
                new Runnable() {
                    @Override
                    public void run() {
                        if (ContentFeatureMap.isEnabled(
                                ContentFeatureList.PROCESS_SHARING_WITH_STRICT_SITE_INSTANCES)) {
                            // If this feature is turned on all the URLs will use the same process
                            // and this test will not observe any kills.
                            Assert.assertEquals(1, connections.size());
                            Assert.assertFalse(connections.get(0).isKilledByUs());
                        } else {
                            // The file: URLs and data: URL are expected to be in different
                            // processes and the data: URL is expected to kill the process used
                            // for the file:
                            // URLs.
                            // Note: The default SiteInstance process model also follows this path
                            // because
                            // file: URLs are not allowed in the default SiteInstance process while
                            // data:
                            // URLs are.
                            Assert.assertEquals(2, connections.size());
                            Assert.assertTrue(connections.get(0).isKilledByUs());
                        }
                    }
                });
    }

    private static class CrashOnLaunchChildProcessConnection extends TestChildProcessConnection {
        private boolean mCrashServiceCalled;
        private final CountDownLatch mDisconnectedLatch = new CountDownLatch(1);
        // Arguments to setupConnection
        private Bundle mConnectionBundle;
        private List<IBinder> mClientInterfaces;
        private IBinder mBinderBox;
        private ConnectionCallback mConnectionCallback;

        public CrashOnLaunchChildProcessConnection(
                Context context,
                ComponentName serviceName,
                boolean bindToCaller,
                boolean bindAsExternalService,
                Bundle childProcessCommonParameters) {
            super(
                    context,
                    serviceName,
                    bindToCaller,
                    bindAsExternalService,
                    childProcessCommonParameters);
        }

        @Override
        protected void onServiceConnectedOnLauncherThread(IBinder service) {
            super.onServiceConnectedOnLauncherThread(service);
            crashServiceForTesting();
            mCrashServiceCalled = true;
            if (mConnectionBundle != null) {
                super.setupConnection(
                        mConnectionBundle,
                        mClientInterfaces,
                        mBinderBox,
                        mConnectionCallback,
                        null);
                mConnectionBundle = null;
                mClientInterfaces = null;
                mBinderBox = null;
                mConnectionCallback = null;
            }
        }

        @Override
        protected void onServiceDisconnectedOnLauncherThread() {
            super.onServiceDisconnectedOnLauncherThread();
            mDisconnectedLatch.countDown();
        }

        @Override
        public void setupConnection(
                Bundle connectionBundle,
                List<IBinder> clientInterfaces,
                IBinder binderBox,
                ConnectionCallback connectionCallback,
                ZygoteInfoCallback zygoteInfoCallback) {
            // Make sure setupConnection is called after crashServiceForTesting so that
            // setupConnection is guaranteed to fail.
            if (mCrashServiceCalled) {
                super.setupConnection(
                        connectionBundle, clientInterfaces, binderBox, connectionCallback, null);
                return;
            }
            mConnectionBundle = connectionBundle;
            mClientInterfaces = clientInterfaces;
            mBinderBox = binderBox;
            mConnectionCallback = connectionCallback;
        }

        public void waitForDisconnect() throws InterruptedException {
            mDisconnectedLatch.await();
        }
    }

    private static class CrashOnLaunchChildProcessConnectionFactory
            extends TestChildProcessConnectionFactory {
        // Only create one CrashOnLaunchChildProcessConnection.
        private CrashOnLaunchChildProcessConnection mCrashConnection;

        @Override
        public ChildProcessConnection createConnection(
                Context context,
                ComponentName serviceName,
                ComponentName fallbackServiceName,
                boolean bindToCaller,
                boolean bindAsExternalService,
                Bundle serviceBundle,
                String instanceName) {
            if (mCrashConnection == null) {
                mCrashConnection =
                        new CrashOnLaunchChildProcessConnection(
                                context,
                                serviceName,
                                bindToCaller,
                                bindAsExternalService,
                                serviceBundle);
                return mCrashConnection;
            }
            return super.createConnection(
                    context,
                    serviceName,
                    fallbackServiceName,
                    bindToCaller,
                    bindAsExternalService,
                    serviceBundle,
                    instanceName);
        }

        public CrashOnLaunchChildProcessConnection getCrashConnection() {
            return mCrashConnection;
        }
    }

    @Test
    @MediumTest
    public void testCrashOnLaunch() throws Throwable {
        final CrashOnLaunchChildProcessConnectionFactory factory =
                new CrashOnLaunchChildProcessConnectionFactory();
        ChildProcessLauncherHelperImpl.setSandboxServicesSettingsForTesting(
                factory, 2, null /* use default service name */);

        // Load url which should fail.
        String url = UrlUtils.getIsolatedTestFileUrl("content/test/data/android/title1.html");
        ContentShellActivity activity = mActivityTestRule.launchContentShellWithUrl(url);

        // Poll until connection is allocated, then wait until connection is disconnected.
        CriteriaHelper.pollInstrumentationThread(
                () ->
                        ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                                () -> factory.getCrashConnection() != null),
                "The connection wasn't established.");
        CrashOnLaunchChildProcessConnection crashConnection =
                ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                        () -> factory.getCrashConnection());
        crashConnection.waitForDisconnect();

        // Load a new URL and make sure everything is ok.
        NavigationController navigationController =
                mActivityTestRule.getWebContents().getNavigationController();
        TestCallbackHelperContainer testCallbackHelperContainer =
                new TestCallbackHelperContainer(activity.getActiveWebContents());
        mActivityTestRule.loadUrl(
                navigationController,
                testCallbackHelperContainer,
                new LoadUrlParams(
                        UrlUtils.getIsolatedTestFileUrl(
                                "content/test/data/android/geolocation.html")));
        mActivityTestRule.waitForActiveShellToBeDoneLoading();
        Assert.assertTrue(factory.getConnections().size() > 0);
    }
}