blob: c53219884e24ec4b2c6d39c92a44d7c7a33f3cb5 [file] [log] [blame]
/*
* 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 com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import javax.annotation.Nullable;
/**
* 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 = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(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;
}
}
}