com.facebook.buck.cli.AdbCommandRunner.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.cli.AdbCommandRunner.java

Source

/*
 * Copyright 2012-present Facebook, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.facebook.buck.cli;

import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.MultiLineReceiver;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.util.TriState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import javax.annotation.Nullable;

import static com.facebook.buck.util.concurrent.MoreExecutors.newMultiThreadExecutor;
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;

/**
 * Base class for commands that use the AndroidDebugBridge to run commands on devices.
 * Currently, {@link InstallCommand}, {@link UninstallCommand}.
 *
 * @param <T>
 */
public abstract class AdbCommandRunner<T extends AbstractCommandOptions> extends AbstractCommandRunner<T> {

    private static final long ADB_CONNECT_TIMEOUT_MS = 5000;
    private static final long ADB_CONNECT_TIME_STEP_MS = ADB_CONNECT_TIMEOUT_MS / 10;

    // Taken from ddms source code.
    final static int INSTALL_TIMEOUT = 2 * 60 * 1000; // 2 min
    final static int GETPROP_TIMEOUT = 2 * 1000; // 2 seconds

    protected AdbCommandRunner(CommandRunnerParams params) {
        super(params);
    }

    /**
     * Returns list of devices that pass the filter. If there is an invalid combination or no
     * devices are left after filtering this function prints an error and returns null.
     */
    @Nullable
    @VisibleForTesting
    List<IDevice> filterDevices(IDevice[] allDevices, AdbOptions adbOptions, TargetDeviceOptions deviceOptions) {
        if (allDevices.length == 0) {
            console.printBuildFailure("No devices are found.");
            return null;
        }

        List<IDevice> devices = Lists.newArrayList();
        TriState emulatorsOnly = TriState.UNSPECIFIED;
        if (deviceOptions.isEmulatorsOnlyModeEnabled() && adbOptions.isMultiInstallModeEnabled()) {
            emulatorsOnly = TriState.UNSPECIFIED;
        } else if (deviceOptions.isEmulatorsOnlyModeEnabled()) {
            emulatorsOnly = TriState.TRUE;
        } else if (deviceOptions.isRealDevicesOnlyModeEnabled()) {
            emulatorsOnly = TriState.FALSE;
        }

        int onlineDevices = 0;
        for (IDevice device : allDevices) {
            boolean passed = false;
            if (device.isOnline()) {
                onlineDevices++;

                boolean serialMatches = true;
                if (deviceOptions.hasSerialNumber()) {
                    serialMatches = device.getSerialNumber().equals(deviceOptions.getSerialNumber());
                }

                boolean deviceTypeMatches;
                if (emulatorsOnly.isSet()) {
                    // Only devices of specific type are accepted:
                    // either real devices only or emulators only.
                    deviceTypeMatches = (emulatorsOnly.asBoolean() == device.isEmulator());
                } else {
                    // All online devices match.
                    deviceTypeMatches = true;
                }
                passed = serialMatches && deviceTypeMatches;
            }

            if (passed) {
                devices.add(device);
            }
        }

        // Filtered out all devices.
        if (onlineDevices == 0) {
            console.printBuildFailure("No devices are found.");
            return null;
        }

        if (devices.isEmpty()) {
            console.printBuildFailure(String.format(
                    "Found %d connected device(s), but none of them matches specified filter.", onlineDevices));
            return null;
        }

        // Found multiple devices but multi-install mode is not enabled.
        if (!adbOptions.isMultiInstallModeEnabled() && devices.size() > 1) {
            console.printBuildFailure(String.format(
                    "%d device(s) matches specified device filter (1 expected).\n"
                            + "Either disconnect other devices or enable multi-install mode (%s).",
                    devices.size(), AdbOptions.MULTI_INSTALL_MODE_SHORT_ARG));
            return null;
        }

        // Report if multiple devices are matching the filter.
        if (devices.size() > 1) {
            console.getStdOut().printf("Found " + devices.size() + " matching devices.\n");
        }
        return devices;
    }

    @VisibleForTesting
    boolean isAdbInitialized(AndroidDebugBridge adb) {
        return adb.isConnected() && adb.hasInitialDeviceList();
    }

    /**
     * Creates connection to adb and waits for this connection to be initialized
     * and receive initial list of devices.
     */
    @VisibleForTesting
    @Nullable
    @SuppressWarnings("PMD.EmptyCatchBlock")
    AndroidDebugBridge createAdb(ExecutionContext context) {
        try {
            AndroidDebugBridge.init(/* clientSupport */ false);
        } catch (IllegalStateException ex) {
            // ADB was already initialized, we're fine, so just ignore.
        }

        AndroidDebugBridge adb = null;
        if (context != null) {
            adb = AndroidDebugBridge.createBridge(context.getPathToAdbExecutable(), false);
        } else {
            adb = AndroidDebugBridge.createBridge();
        }
        if (adb == null) {
            console.printBuildFailure("Failed to connect to adb. Make sure adb server is running.");
            return null;
        }

        long start = System.currentTimeMillis();
        while (!isAdbInitialized(adb)) {
            long timeLeft = start + ADB_CONNECT_TIMEOUT_MS - System.currentTimeMillis();
            if (timeLeft <= 0) {
                break;
            }
            try {
                Thread.sleep(ADB_CONNECT_TIME_STEP_MS);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        return isAdbInitialized(adb) ? adb : null;
    }

    /**
     * Execute an {@link AdbCallable} for all matching devices. This functions performs device
     * filtering based on three possible arguments:
     *
     *  -e (emulator-only) - only emulators are passing the filter
     *  -d (device-only) - only real devices are passing the filter
     *  -s (serial) - only device/emulator with specific serial number are passing the filter
     *
     *  If more than one device matches the filter this function will fail unless multi-install
     *  mode is enabled (-x). This flag is used as a marker that user understands that multiple
     *  devices will be used to install the apk if needed.
     */
    @VisibleForTesting
    protected boolean adbCall(AdbOptions options, TargetDeviceOptions deviceOptions, ExecutionContext context,
            AdbCallable adbCallable) {

        // Initialize adb connection.
        AndroidDebugBridge adb = createAdb(context);
        if (adb == null) {
            console.printBuildFailure("Failed to create adb connection.");
            return false;
        }

        // Build list of matching devices.
        List<IDevice> devices = filterDevices(adb.getDevices(), options, deviceOptions);
        if (devices == null) {
            return false;
        }

        int adbThreadCount = options.getAdbThreadCount();
        if (adbThreadCount <= 0) {
            adbThreadCount = devices.size();
        }

        // Start executions on all matching devices.
        List<ListenableFuture<Boolean>> futures = Lists.newArrayList();
        ListeningExecutorService executorService = listeningDecorator(
                newMultiThreadExecutor(getClass().getSimpleName(), adbThreadCount));
        for (final IDevice device : devices) {
            futures.add(executorService.submit(adbCallable.forDevice(device)));
        }

        // Wait for all executions to complete or fail.
        List<Boolean> results = null;
        try {
            results = Futures.allAsList(futures).get();
        } catch (ExecutionException ex) {
            console.printBuildFailure("Failed: " + adbCallable);
            ex.printStackTrace(console.getStdErr());
            return false;
        } catch (InterruptedException ex) {
            console.printBuildFailure("Interrupted.");
            ex.printStackTrace(console.getStdErr());
            return false;
        } finally {
            executorService.shutdownNow();
        }

        int successCount = 0;
        for (Boolean result : results) {
            if (result) {
                successCount++;
            }
        }
        int failureCount = results.size() - successCount;

        // Report results.
        if (successCount > 0) {
            console.printSuccess(String.format("Succesfully ran %s on %d device(s)", adbCallable, successCount));
        }
        if (failureCount > 0) {
            console.printBuildFailure(String.format("Failed to %s on %d device(s).", adbCallable, failureCount));
        }

        return failureCount == 0;
    }

    /**
     * Base class for commands to be run against an {@link com.android.ddmlib.IDevice IDevice}.
     */
    public abstract class AdbCallable {

        /**
         * Perform the actions specified by this {@code AdbCallable} and return true on success.
         * @param device the {@link com.android.ddmlib.IDevice IDevice} to run against
         * @return {@code true} if the command succeeded.
         */
        public abstract boolean call(IDevice device) throws Exception;

        /**
         * Wraps this as a {@link java.util.concurrent.Callable Callable&lt;Boolean&gt;} whose
         * {@link Callable#call() call()} method calls
         * {@link AdbCommandRunner.AdbCallable#call(IDevice) call(IDevice)} against the specified
         * device.
         * @param device the {@link com.android.ddmlib.IDevice IDevice} to run against.
         * @return a {@code Callable}
         */
        public Callable<Boolean> forDevice(final IDevice device) {
            return new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    return AdbCallable.this.call(device);
                }

                @Override
                public String toString() {
                    return AdbCallable.this.toString();
                }
            };
        }
    }

    /**
     * Implementation of {@link com.android.ddmlib.IShellOutputReceiver} with helper functions to
     * parse output lines and figure out if a call to
     * {@link com.android.ddmlib.IDevice#executeShellCommand(String,
     * com.android.ddmlib.IShellOutputReceiver)} succeeded.
     */
    protected static abstract class ErrorParsingReceiver extends MultiLineReceiver {

        private String errorMessage = null;

        /**
         * Look for an error message in {@code line}.
         * @param line
         * @return an error message if {@code line} is indicative of an error, {@code null} otherwise.
         */
        protected abstract String matchForError(String line);

        /**
         * Look for a message indicating success - the error message is reset if this returns
         * {@code true}.
         * @param line
         * @return {@code true} if this line indicates success.
         */
        protected boolean matchForSuccess(String line) {
            return false;
        }

        @Override
        public void processNewLines(String[] lines) {
            for (String line : lines) {
                if (line.length() > 0) {
                    if (matchForSuccess(line)) {
                        errorMessage = null;
                    } else {
                        String err = matchForError(line);
                        if (err != null) {
                            errorMessage = err;
                        }
                    }
                }
            }
        }

        @Override
        public boolean isCancelled() {
            return false;
        }

        public String getErrorMessage() {
            return errorMessage;
        }
    }

}