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

// Copyright 2014 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.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.BaseSwitches;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.process_launcher.ChildConnectionAllocator;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.process_launcher.FileDescriptorInfo;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.content_public.browser.test.ContentJUnit4ClassRunner;
import org.chromium.content_shell_apk.ChildProcessLauncherTestHelperService;
import org.chromium.content_shell_apk.ChildProcessLauncherTestUtils;
import org.chromium.content_shell_apk.ContentShellActivity;
import org.chromium.content_shell_apk.ContentShellActivityTestRule;

import java.util.concurrent.Callable;

/** Instrumentation tests for ChildProcessLauncher. */
@RunWith(ContentJUnit4ClassRunner.class)
public class ChildProcessLauncherHelperTest {
    // Pseudo command line arguments to instruct the child process to wait until being killed.
    // Allowing the process to continue would lead to a crash when attempting to initialize IPC
    // channels that are not being set up in this test.
    private static final String[] sProcessWaitArguments = {
        "_", "--" + BaseSwitches.RENDERER_WAIT_FOR_JAVA_DEBUGGER
    };
    private static final String DEFAULT_SANDBOXED_PROCESS_SERVICE =
            "org.chromium.content.app.SandboxedProcessService";

    private static final int DONT_BLOCK = 0;
    private static final int BLOCK_UNTIL_CONNECTED = 1;
    private static final int BLOCK_UNTIL_SETUP = 2;

    @Rule
    public ContentShellActivityTestRule mActivityTestRule = new ContentShellActivityTestRule();

    @Before
    public void setUp() {
        LibraryLoader.getInstance().ensureInitialized();
    }

    /**
     * Tests binding to the same sandboxed service process from multiple processes in the same
     * package. This uses the ChildProcessLauncherTestHelperService declared in ContentShell.apk as
     * a separate android:process to bind the first (slot 0) service. The instrumentation test then
     * tries to bind the same slot, which fails, so the ChildProcessLauncher retries on a new
     * connection.
     */
    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    @DisabledTest(message = "Flaky - crbug.com/752691")
    public void testBindServiceFromMultipleProcesses() throws RemoteException {
        ChildProcessLauncherHelperImpl.setSandboxServicesSettingsForTesting(
                /* factory= */ null, 2, DEFAULT_SANDBOXED_PROCESS_SERVICE);
        final Context context = InstrumentationRegistry.getTargetContext();

        // Start the Helper service.
        class HelperConnection implements ServiceConnection {
            Messenger mMessenger;

            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mMessenger = new Messenger(service);
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {}
        }
        final HelperConnection serviceConnection = new HelperConnection();

        Intent intent = new Intent();
        intent.setComponent(
                new ComponentName(
                        context.getPackageName(),
                        context.getPackageName() + ".ChildProcessLauncherTestHelperService"));
        Assert.assertTrue(context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE));

        // Wait for the Helper service to connect.
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Failed to get helper service Messenger",
                            serviceConnection.mMessenger,
                            Matchers.notNullValue());
                });

        Assert.assertNotNull(serviceConnection.mMessenger);

        class ReplyHandler implements Handler.Callback {
            Message mMessage;

            @Override
            public boolean handleMessage(Message msg) {
                // Copy the message so its contents outlive this Binder transaction.
                mMessage = Message.obtain();
                mMessage.copyFrom(msg);
                return true;
            }
        }
        final ReplyHandler replyHandler = new ReplyHandler();

        // Send a message to the Helper and wait for the reply. This will cause the slot 0
        // sandboxed service connection to be bound by a different PID (i.e., not this process).
        Message msg = Message.obtain(null, ChildProcessLauncherTestHelperService.MSG_BIND_SERVICE);
        msg.replyTo = new Messenger(new Handler(Looper.getMainLooper(), replyHandler));
        serviceConnection.mMessenger.send(msg);

        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Failed waiting for helper service reply",
                            replyHandler.mMessage,
                            Matchers.notNullValue());
                });

        // Verify that the Helper was able to launch the sandboxed service.
        Assert.assertNotNull(replyHandler.mMessage);
        Assert.assertEquals(
                ChildProcessLauncherTestHelperService.MSG_BIND_SERVICE_REPLY,
                replyHandler.mMessage.what);
        Assert.assertEquals(
                "Connection slot from helper service is not 0", 0, replyHandler.mMessage.arg2);

        final int helperConnectionPid = replyHandler.mMessage.arg1;
        Assert.assertTrue(helperConnectionPid > 0);

        // Launch a service from this process. Since slot 0 is already bound by the Helper, it
        // will fail to start and the ChildProcessLauncher will retry and use the slot 1.
        ChildProcessCreationParamsImpl.set(
                context.getPackageName(),
                /* privilegedServicesName= */ null,
                context.getPackageName(),
                /* sandboxedServicesName= */ null,
                /* isExternalService= */ false,
                LibraryProcessType.PROCESS_CHILD,
                /* bindToCallerCheck= */ true,
                /* ignoreVisibilityForImportance= */ false);
        ChildProcessLauncherHelperImpl launcher =
                startSandboxedChildProcess(BLOCK_UNTIL_SETUP, /* doSetupConnection= */ true);

        final ChildProcessConnection retryConnection =
                ChildProcessLauncherTestUtils.getConnection(launcher);
        Assert.assertEquals(
                1, ChildProcessLauncherTestUtils.getConnectionServiceNumber(retryConnection));

        ChildConnectionAllocator.FixedSizeAllocatorImpl connectionAllocator =
                (ChildConnectionAllocator.FixedSizeAllocatorImpl)
                        launcher.getChildConnectionAllocatorForTesting();

        // Check that only one connection is created.
        for (int i = 0; i < connectionAllocator.getNumberOfServices(); ++i) {
            ChildProcessConnection sandboxedConn =
                    connectionAllocator.getChildProcessConnectionAtSlotForTesting(i);
            if (i == 1) {
                Assert.assertNotNull(sandboxedConn);
                Assert.assertNotNull(
                        ChildProcessLauncherTestUtils.getConnectionService(sandboxedConn));
            } else {
                Assert.assertNull(sandboxedConn);
            }
        }

        Assert.assertEquals(
                connectionAllocator.getChildProcessConnectionAtSlotForTesting(1), retryConnection);

        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Failed waiting retry connection to get pid",
                            ChildProcessLauncherTestUtils.getConnectionPid(retryConnection),
                            Matchers.greaterThan(0));
                });
        Assert.assertTrue(
                ChildProcessLauncherTestUtils.getConnectionPid(retryConnection)
                        != helperConnectionPid);
        Assert.assertTrue(
                ChildProcessLauncherTestUtils.getConnectionService(retryConnection)
                        .bindToCaller(ChildProcessConnection.getBindToCallerClazz()));

        // Unbind the service.
        replyHandler.mMessage = null;
        msg = Message.obtain(null, ChildProcessLauncherTestHelperService.MSG_UNBIND_SERVICE);
        msg.replyTo = new Messenger(new Handler(Looper.getMainLooper(), replyHandler));
        serviceConnection.mMessenger.send(msg);
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Failed waiting for helper service unbind reply",
                            replyHandler.mMessage,
                            Matchers.notNullValue());
                });
        Assert.assertEquals(
                ChildProcessLauncherTestHelperService.MSG_UNBIND_SERVICE_REPLY,
                replyHandler.mMessage.what);

        // The 0th connection should now be usable.
        launcher = startSandboxedChildProcess(BLOCK_UNTIL_SETUP, /* doSetupConnection= */ true);
        ChildProcessConnection connection = ChildProcessLauncherTestUtils.getConnection(launcher);
        Assert.assertEquals(
                0, ChildProcessLauncherTestUtils.getConnectionServiceNumber(connection));
    }

    private static void warmUpOnUiThreadBlocking(final Context context, boolean sandboxed) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ChildProcessLauncherHelperImpl.warmUpOnAnyThread(context, sandboxed);
                });
        ChildProcessConnection connection = getWarmUpConnection(sandboxed);
        Assert.assertNotNull(connection);
        blockUntilConnected(connection);
    }

    private void testWarmUpImpl() {
        Context context = InstrumentationRegistry.getTargetContext();
        warmUpOnUiThreadBlocking(context, /* sandboxed= */ true);

        Assert.assertEquals(1, getConnectedSandboxedServicesCount());

        ChildProcessLauncherHelperImpl launcherHelper =
                startSandboxedChildProcess(BLOCK_UNTIL_SETUP, /* doSetupConnection= */ true);

        // The warm-up connection was used, so no new process should have been created.
        Assert.assertEquals(1, getConnectedSandboxedServicesCount());

        int pid = getPid(launcherHelper);
        Assert.assertNotEquals(0, pid);

        stopProcess(launcherHelper);

        waitForConnectedSandboxedServicesCount(0);
    }

    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testWarmUp() {
        // Use the default creation parameters.
        testWarmUpImpl();
    }

    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testWarmUpWithBindToCaller() {
        Context context = InstrumentationRegistry.getTargetContext();
        ChildProcessCreationParamsImpl.set(
                context.getPackageName(),
                /* privilegedServicesName= */ null,
                context.getPackageName(),
                /* sandboxedServicesName= */ null,
                /* isExternalService= */ false,
                LibraryProcessType.PROCESS_CHILD,
                /* bindToCallerCheck= */ true,
                /* ignoreVisibilityForImportance= */ false);
        testWarmUpImpl();
    }

    // Tests that the warm-up connection is freed from its allocator if it crashes.
    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testWarmUpProcessCrashBeforeUse() {
        Assert.assertEquals(0, getConnectedSandboxedServicesCount());

        Context context = InstrumentationRegistry.getTargetContext();
        warmUpOnUiThreadBlocking(context, /* sandboxed= */ true);

        Assert.assertEquals(1, getConnectedSandboxedServicesCount());

        // Crash the warm-up connection before it gets used.
        ChildProcessConnection connection = getWarmUpConnection(/* sandboxed= */ true);
        Assert.assertNotNull(connection);
        connection.crashServiceForTesting();

        // It should get cleaned-up.
        waitForConnectedSandboxedServicesCount(0);

        // And subsequent process launches should work.
        ChildProcessLauncherHelperImpl launcher =
                startSandboxedChildProcess(BLOCK_UNTIL_SETUP, /* doSetupConnection= */ true);
        Assert.assertEquals(1, getConnectedSandboxedServicesCount());
        Assert.assertNotNull(ChildProcessLauncherTestUtils.getConnection(launcher));
    }

    // Tests that the warm-up connection is freed from its allocator if it crashes after being used.
    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testWarmUpProcessCrashAfterUse() {
        Context context = InstrumentationRegistry.getTargetContext();
        warmUpOnUiThreadBlocking(context, /* sandboxed= */ true);

        Assert.assertEquals(1, getConnectedSandboxedServicesCount());

        ChildProcessLauncherHelperImpl launcherHelper =
                startSandboxedChildProcess(BLOCK_UNTIL_SETUP, /* doSetupConnection= */ true);

        // The warm-up connection was used, so no new process should have been created.
        Assert.assertEquals(1, getConnectedSandboxedServicesCount());

        int pid = getPid(launcherHelper);
        Assert.assertNotEquals(0, pid);

        ChildProcessConnection connection = retrieveConnection(launcherHelper);
        connection.crashServiceForTesting();

        waitForConnectedSandboxedServicesCount(0);
    }

    // Tests that the warm-up the previleged process connection.
    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testWarmUpPrivilegedProcess() {
        Assert.assertEquals(0, getConnectedServicesCount());

        Context context = InstrumentationRegistry.getTargetContext();
        warmUpOnUiThreadBlocking(context, /* sandboxed= */ false);

        Assert.assertEquals(0, getConnectedSandboxedServicesCount());
        Assert.assertEquals(1, getConnectedServicesCount());

        // And subsequent process launches should work.
        ChildProcessLauncherHelperImpl launcher =
                startChildProcess(
                        BLOCK_UNTIL_SETUP,
                        /* doSetupConnection= */ true,
                        /* sandboxed= */ false,
                        /* reducePriorityOnBackground= */ false,
                        /* canUseWarmUpConnection= */ true);
        Assert.assertEquals(1, getConnectedServicesCount());
        Assert.assertEquals(0, getConnectedSandboxedServicesCount());
        Assert.assertNotNull(ChildProcessLauncherTestUtils.getConnection(launcher));
    }

    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testLauncherCleanup() {
        ChildProcessLauncherHelperImpl launcher =
                startSandboxedChildProcess(BLOCK_UNTIL_SETUP, /* doSetupConnection= */ true);
        int pid = getPid(launcher);
        Assert.assertNotEquals(0, pid);

        // Stop the process explicitly, the launcher should get cleared.
        stopProcess(launcher);
        waitForConnectedSandboxedServicesCount(0);

        launcher = startSandboxedChildProcess(BLOCK_UNTIL_SETUP, /* doSetupConnection= */ true);
        pid = getPid(launcher);
        Assert.assertNotEquals(0, pid);

        // This time crash the connection, the launcher should also get cleared.
        ChildProcessConnection connection = retrieveConnection(launcher);
        connection.crashServiceForTesting();
        waitForConnectedSandboxedServicesCount(0);
    }

    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testReducePriorityOnBackground() {
        ChildProcessLauncherHelperImpl.setSkipDelayForReducePriorityOnBackgroundForTesting();

        final ContentShellActivity activity =
                mActivityTestRule.launchContentShellWithUrl("about:blank");
        mActivityTestRule.waitForActiveShellToBeDoneLoading();
        Assert.assertTrue(ApplicationStatus.hasVisibleActivities());

        ChildProcessLauncherHelperImpl launcher =
                startChildProcess(
                        BLOCK_UNTIL_SETUP,
                        /* doSetupConnection= */ true,
                        /* sandboxed= */ false,
                        /* reducePriorityOnBackground= */ true,
                        /* canUseWarmUpConnection= */ true);
        final ChildProcessConnection connection =
                ChildProcessLauncherTestUtils.getConnection(launcher);

        Assert.assertTrue(
                ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                        () -> connection.isStrongBindingBound()));

        ThreadUtils.runOnUiThreadBlocking(
                () -> ApplicationStatus.onStateChangeForTesting(activity, ActivityState.STOPPED));
        Assert.assertFalse(ApplicationStatus.hasVisibleActivities());
        Assert.assertFalse(
                ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                        () -> connection.isStrongBindingBound()));
    }

    @Test
    @MediumTest
    @Feature({"ProcessManagement"})
    public void testLaunchWithReducedPriorityOnBackground() {
        ChildProcessLauncherHelperImpl.setSkipDelayForReducePriorityOnBackgroundForTesting();

        final ContentShellActivity activity =
                mActivityTestRule.launchContentShellWithUrl("about:blank");
        mActivityTestRule.waitForActiveShellToBeDoneLoading();
        ThreadUtils.runOnUiThreadBlocking(
                () -> ApplicationStatus.onStateChangeForTesting(activity, ActivityState.STOPPED));
        Assert.assertFalse(ApplicationStatus.hasVisibleActivities());

        ChildProcessLauncherHelperImpl launcher =
                startChildProcess(
                        BLOCK_UNTIL_SETUP,
                        /* doSetupConnection= */ true,
                        /* sandboxed= */ false,
                        /* reducePriorityOnBackground= */ true,
                        /* canUseWarmUpConnection= */ true);
        final ChildProcessConnection connection =
                ChildProcessLauncherTestUtils.getConnection(launcher);

        Assert.assertFalse(
                ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                        () -> connection.isStrongBindingBound()));
    }

    private static ChildProcessLauncherHelperImpl startSandboxedChildProcess(
            int blockingPolicy, final boolean doSetupConnection) {
        return startChildProcess(
                blockingPolicy,
                doSetupConnection,
                /* sandboxed= */ true,
                /* reducePriorityOnBackground= */ false,
                /* canUseWarmUpConnection= */ true);
    }

    private static ChildProcessLauncherHelperImpl startChildProcess(
            int blockingPolicy,
            final boolean doSetupConnection,
            boolean sandboxed,
            boolean reducePriorityOnBackground,
            boolean canUseWarmUpConnection) {
        assert doSetupConnection || blockingPolicy != BLOCK_UNTIL_SETUP;
        ChildProcessLauncherHelperImpl launcher =
                ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                        new Callable<ChildProcessLauncherHelperImpl>() {
                            @Override
                            public ChildProcessLauncherHelperImpl call() {
                                return ChildProcessLauncherHelperImpl.createAndStartForTesting(
                                        sProcessWaitArguments,
                                        new FileDescriptorInfo[0],
                                        sandboxed,
                                        reducePriorityOnBackground,
                                        canUseWarmUpConnection,
                                        /* binderCallback= */ null,
                                        doSetupConnection);
                            }
                        });
        if (blockingPolicy != DONT_BLOCK) {
            assert blockingPolicy == BLOCK_UNTIL_CONNECTED || blockingPolicy == BLOCK_UNTIL_SETUP;
            blockUntilConnected(launcher);
            if (blockingPolicy == BLOCK_UNTIL_SETUP) {
                blockUntilSetup(launcher);
            }
        }
        return launcher;
    }

    private static void blockUntilConnected(final ChildProcessLauncherHelperImpl launcher) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            launcher.getChildProcessConnection(), Matchers.notNullValue());
                    Criteria.checkThat(
                            launcher.getChildProcessConnection().isConnected(), Matchers.is(true));
                });
    }

    private static void blockUntilConnected(final ChildProcessConnection connection) {
        CriteriaHelper.pollInstrumentationThread(
                connection::isConnected, "The connection wasn't established.");
    }

    private static void blockUntilSetup(final ChildProcessLauncherHelperImpl launcher) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "The connection wasn't established", getPid(launcher), Matchers.not(0));
                });
    }

    // Returns the number of all connection currently connected.
    private static int getConnectedServicesCount() {
        return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                new Callable<Integer>() {
                    @Override
                    public Integer call() {
                        return ChildProcessLauncherHelperImpl.getConnectedServicesCountForTesting();
                    }
                });
    }

    // Returns the number of sandboxed connection currently connected,
    private static int getConnectedSandboxedServicesCount() {
        return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                new Callable<Integer>() {
                    @Override
                    public Integer call() {
                        return ChildProcessLauncherHelperImpl
                                .getConnectedSandboxedServicesCountForTesting();
                    }
                });
    }

    // Blocks until the number of sandboxed connections reaches targetCount.
    private static void waitForConnectedSandboxedServicesCount(int targetCount) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            getConnectedSandboxedServicesCount(), Matchers.is(targetCount));
                });
    }

    private static ChildProcessConnection retrieveConnection(
            final ChildProcessLauncherHelperImpl launcherHelper) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    Criteria.checkThat(
                            "Failed waiting for child process to connect",
                            ChildProcessLauncherTestUtils.getConnection(launcherHelper),
                            Matchers.notNullValue());
                });
        return ChildProcessLauncherTestUtils.getConnection(launcherHelper);
    }

    private static void stopProcess(ChildProcessLauncherHelperImpl launcherHelper) {
        final ChildProcessConnection connection = retrieveConnection(launcherHelper);
        ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
                new Runnable() {
                    @Override
                    public void run() {
                        ChildProcessLauncherHelperImpl.stop(connection.getPid());
                    }
                });
    }

    private static void stopProcesses(ChildProcessLauncherHelperImpl... launcherHelpers) {
        final int[] pids = new int[launcherHelpers.length];
        for (int i = 0; i < launcherHelpers.length; i++) {
            pids[i] = getPid(launcherHelpers[i]);
        }
        ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
                new Runnable() {
                    @Override
                    public void run() {
                        for (int pid : pids) {
                            ChildProcessLauncherHelperImpl.stop(pid);
                        }
                    }
                });
    }

    private static int getPid(final ChildProcessLauncherHelperImpl launcherHelper) {
        return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                new Callable<Integer>() {
                    @Override
                    public Integer call() {
                        return launcherHelper.getPidForTesting();
                    }
                });
    }

    private static ChildProcessConnection getWarmUpConnection(boolean sandboxed) {
        return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
                new Callable<ChildProcessConnection>() {
                    @Override
                    public ChildProcessConnection call() {
                        return ChildProcessLauncherHelperImpl.getWarmUpConnectionForTesting(
                                sandboxed);
                    }
                });
    }
}