blob: 397e6f1661c6c6bf64740cd2ffce6fa86076a61a [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.android.tradefed.device;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.Log;
import com.android.ddmlib.SyncService;
import com.android.ddmlib.SyncService.SyncResult;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.tradefed.util.RunUtil;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Default implementation of a {@link ITestDevice}
*/
class TestDevice implements ILogTestDevice {
private static final String LOG_TAG = "TestDevice";
/** the default number of command retry attempts to perform */
private static final int MAX_RETRY_ATTEMPTS = 3;
/** time in ms to wait before retrying a logcat operation if interrupted */
private static final int LOGCAT_SLEEP_TIME = 10*1000;
/** the max number of bytes to store in logcat tmp buffer */
private static final int LOGCAT_BUFF_SIZE = 32 * 1024;
private static final String LOGCAT_CMD = "logcat -v threadtime";
/**
* The maximum number of bytes returned by {@link TestDevice#getLogcat()}.
* TODO: make this configurable
*/
private static final int LOGCAT_MAX_DATA = 512 * 1024;
/** The time in ms to wait for a device to be back online after a adb root. */
private static final int ROOT_TIMEOUT = 1 * 60 * 1000;
/** The time in ms to wait for a device to boot into fastboot. */
private static final int FASTBOOT_TIMEOUT = 1 * 60 * 1000;
private final IDevice mIDevice;
private final IDeviceRecovery mRecovery;
private final IDeviceStateMonitor mMonitor;
// TODO: make this an Option - consider moving postBootSetup elsewhere
private boolean mEnableAdbRoot = true;
private LogCatReceiver mLogcatReceiver;
/**
* Interface for a generic device communication attempt.
*/
private static interface IDeviceAction {
/**
* Perform action that requires device communication
* @return <code>true</code> if command succeeded. <code>false</code> otherwise.
* @throws IOException if device communication failed
*/
boolean doAction() throws IOException;
}
/**
* Creates a {@link TestDevice}.
*
* @param device the associated {@link IDevice}
* @param recovery the {@link IDeviceRecovery} mechanism to use
*/
TestDevice(IDevice device, IDeviceRecovery recovery, IDeviceStateMonitor monitor) {
mIDevice = device;
mRecovery = recovery;
mMonitor = monitor;
}
/**
* {@inheritDoc}
*/
public IDevice getIDevice() {
return mIDevice;
}
/**
* {@inheritDoc}
*/
public String getSerialNumber() {
return getIDevice().getSerialNumber();
}
/**
* {@inheritDoc}
*/
public void executeShellCommand(final String command, final IShellOutputReceiver receiver)
throws DeviceNotAvailableException {
IDeviceAction action = new IDeviceAction() {
public boolean doAction() throws IOException {
getIDevice().executeShellCommand(command, receiver);
return true;
}
};
performDeviceAction(String.format("shell %s", command), action, MAX_RETRY_ATTEMPTS);
}
/**
* {@inheritDoc}
*/
public String executeShellCommand(String command) throws DeviceNotAvailableException {
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
executeShellCommand(command, receiver);
String output = receiver.getOutput();
Log.d(LOG_TAG, String.format("%s on %s returned %s", command, getSerialNumber(), output));
return output;
}
/**
* {@inheritDoc}
*/
public void runInstrumentationTests(IRemoteAndroidTestRunner runner,
Collection<ITestRunListener> listeners) throws DeviceNotAvailableException {
try {
runner.run(listeners);
} catch (IOException e) {
Log.w(LOG_TAG, String.format("IOException %s when running tests %s on %s",
e.getMessage(), runner.getPackageName(), getSerialNumber()));
for (ITestRunListener listener : listeners) {
listener.testRunFailed("lost connection with device");
}
// TODO: no attempt tracking here. Would be good to catch scenario where repeated
// test runs fail even though recovery is succeeding
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
public boolean pullFile(final String remoteFilePath, final File localFile)
throws DeviceNotAvailableException {
IDeviceAction pullAction = new IDeviceAction() {
public boolean doAction() throws IOException {
SyncService syncService = getIDevice().getSyncService();
SyncResult result = syncService.pullFile(remoteFilePath,
localFile.getAbsolutePath(), SyncService.getNullProgressMonitor());
if (result.getCode() == SyncService.RESULT_OK) {
return true;
} else {
// assume that repeated attempts won't help, just return immediately
Log.w(LOG_TAG, String.format("Failed to pull %s from %s. Reason code: %d, " +
"message %s", remoteFilePath, getSerialNumber(), result.getCode(),
result.getMessage()));
return false;
}
}
};
return performDeviceAction(String.format("pull %s", remoteFilePath), pullAction,
MAX_RETRY_ATTEMPTS);
}
/**
* {@inheritDoc}
*/
public boolean pushFile(final File localFile, final String remoteFilePath)
throws DeviceNotAvailableException {
IDeviceAction pushAction = new IDeviceAction() {
public boolean doAction() throws IOException {
SyncService syncService = getIDevice().getSyncService();
SyncResult result = syncService.pushFile(localFile.getAbsolutePath(),
remoteFilePath, SyncService.getNullProgressMonitor());
if (result.getCode() == SyncService.RESULT_OK) {
return true;
} else {
// assume that repeated attempts won't help, just return immediately
Log.w(LOG_TAG, String.format("Failed to push to %s on device %s. Reason " +
" code: %d, message %s", remoteFilePath, getSerialNumber(),
result.getCode(), result.getMessage()));
return false;
}
}
};
return performDeviceAction(String.format("push %s", remoteFilePath), pushAction,
MAX_RETRY_ATTEMPTS);
}
/**
* {@inheritDoc}
*/
public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
String lsGrep = executeShellCommand(String.format("ls \"%s\"", destPath));
return !lsGrep.contains("No such file or directory");
}
/**
* {@inheritDoc}
*/
public long getExternalStoreFreeSpace() throws DeviceNotAvailableException {
String externalStorePath = getIDevice().getMountPoint(
IDevice.MNT_EXTERNAL_STORAGE);
String output = executeShellCommand(String.format("df %s", externalStorePath));
final Pattern freeSpacePattern = Pattern.compile("(\\d+)K available");
Matcher patternMatcher = freeSpacePattern.matcher(output);
if (patternMatcher.find()) {
String freeSpaceString = patternMatcher.group(1);
try {
return Long.parseLong(freeSpaceString);
} catch (NumberFormatException e) {
// fall through
}
}
Log.e(LOG_TAG, String.format(
"free space command output \"%s\" did not match expected pattern ", output));
return 0;
}
/**
* {@inheritDoc}
*/
public String executeAdbCommand(String... cmdArgs) throws DeviceNotAvailableException {
final String[] fullCmd = buildAdbCommand(cmdArgs);
final String[] output = new String[1];
IDeviceAction adbAction = new IDeviceAction() {
public boolean doAction() throws IOException {
output[0] = RunUtil.runTimedCmd(2*60*1000, fullCmd);
if (output[0] == null) {
throw new IOException();
}
return true;
}
};
performDeviceAction(String.format("adb %s", cmdArgs[0]), adbAction, MAX_RETRY_ATTEMPTS);
return output[0];
}
/**
* {@inheritDoc}
*/
public boolean executeFastbootCommand(String... cmdArgs) throws DeviceNotAvailableException {
final String[] fullCmd = buildFastbootCommand(cmdArgs);
IDeviceAction fastbootAction = new IDeviceAction() {
public boolean doAction() throws IOException {
String result = RunUtil.runTimedCmd(2*60*1000, fullCmd);
if (result == null) {
throw new IOException();
}
return true;
}
};
// TODO: change to do fastboot recovery
return performDeviceAction(String.format("fastboot %s", cmdArgs[0]), fastbootAction,
MAX_RETRY_ATTEMPTS);
}
/**
* Builds the OS command for the given adb command and args
*/
private String[] buildAdbCommand(String... commandArgs) {
final int numAdbArgs = 3;
String[] newCmdArgs = new String[commandArgs.length + numAdbArgs];
// TODO: use full adb path
newCmdArgs[0] = "adb";
newCmdArgs[1] = "-s";
newCmdArgs[2] = getSerialNumber();
System.arraycopy(commandArgs, 0, newCmdArgs, numAdbArgs, commandArgs.length);
return newCmdArgs;
}
/**
* Builds the OS command for the given fastboot command and args
*/
private String[] buildFastbootCommand(String... commandArgs) {
final int numAdbArgs = 3;
String[] newCmdArgs = new String[commandArgs.length + numAdbArgs];
// TODO: use full fastboot path
newCmdArgs[0] = "fastboot";
newCmdArgs[1] = "-s";
newCmdArgs[2] = getSerialNumber();
System.arraycopy(commandArgs, 0, newCmdArgs, numAdbArgs, commandArgs.length);
return newCmdArgs;
}
/**
* Recursively performs an action on this device. Attempts to recover device and retry command
* if action fails.
*
* @param actionDescription a short description of action to be performed. Used for logging
* purposes only.
* @param action the action to be performed
* @param callback optional action to perform if action fails but recovery succeeds. If no
* post recovery action needs to be taken pass in <code>null</code>
* @param remainingAttempts the remaining retry attempts to make for action if it fails but
* recovery succeeds
*
* @returns <code>true</code> if action was performed successfully
*
* @throws DeviceNotAvailableException if recovery attempt fails or max attempts done without
* success
*/
private boolean performDeviceAction(String actionDescription, IDeviceAction action,
int remainingAttempts) throws DeviceNotAvailableException {
try {
return action.doAction();
} catch (IOException e) {
Log.w(LOG_TAG, String.format("IOException %s when attempting %s on device %s",
e.getMessage(), actionDescription, getSerialNumber()));
recoverDevice();
if (remainingAttempts > 0) {
return performDeviceAction(actionDescription, action, remainingAttempts - 1);
} else {
throw new DeviceNotAvailableException(String.format("Attempted %s multiple times " +
"on device %s without communication success. Aborting.", actionDescription,
getSerialNumber()));
}
}
}
/**
* Attempts to recover device communication.
*
* @throws DeviceNotAvailableException if device is not longer available
*/
private void recoverDevice() throws DeviceNotAvailableException {
Log.i(LOG_TAG, String.format("Attempting recovery on %s", getSerialNumber()));
mRecovery.recoverDevice(getIDevice(), mMonitor);
Log.i(LOG_TAG, String.format("Recovery successful for %s", getSerialNumber()));
}
/**
* {@inheritDoc}
*/
public void startLogcat() {
if (mLogcatReceiver != null) {
Log.d(LOG_TAG, String.format("Already capturing logcat for %s, ignoring",
getSerialNumber()));
return;
}
mLogcatReceiver = new LogCatReceiver();
mLogcatReceiver.start();
}
/**
* {@inheritDoc}
* <p/>
* Works in two modes:
* <li>If the logcat is currently being captured in the background (i.e. the
* manager of this device is calling startLogcat and stopLogcat as appropriate), will return
* the current contents of the background logcat capture, up to {@link #LOGCAT_MAX_DATA}.
* <li>Otherwise, will return a static dump of the logcat data if device is currently responding
*/
public InputStream getLogcat() {
if (mLogcatReceiver == null) {
Log.w(LOG_TAG, String.format("Not capturing logcat for %s, returning a dump for",
getSerialNumber()));
return getLogcatDump();
} else {
return mLogcatReceiver.getLogcatData();
}
}
/**
* Get a dump of the current logcat for device, up to a max of {@link #LOGCAT_MAX_DATA}.
*
* @return a {@link InputStream} of the logcat data. An empty stream is returned if fail to
* capture logcat data.
*/
private InputStream getLogcatDump() {
String output = "";
try {
// use IDevice directly because we don't want callers to handle
// DeviceNotAvailableException for this method
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
// add -d parameter to make this a non blocking call
getIDevice().executeShellCommand(LOGCAT_CMD + " -d", receiver);
output = receiver.getOutput();
if (output.length() > LOGCAT_MAX_DATA) {
output = output.substring(LOGCAT_MAX_DATA - output.length(), output.length());
}
} catch (IOException e) {
Log.w(LOG_TAG, String.format("Failed to get logcat dump %s", getSerialNumber()));
}
return new ByteArrayInputStream(output.getBytes());
}
/**
* {@inheritDoc}
*/
public void stopLogcat() {
if (mLogcatReceiver != null) {
mLogcatReceiver.cancel();
mLogcatReceiver = null;
} else {
Log.w(LOG_TAG, String.format("Attempting to stop logcat when not capturing for %s",
getSerialNumber()));
}
}
/**
* A background thread that captures logcat data into a temporary host file.
* <p/>
* This is done so:
* <li>if device goes permanently offline during a test, the log data is retained.
* <li>to capture more data than may fit in device's circular log.
*/
private class LogCatReceiver extends Thread implements IShellOutputReceiver {
private boolean mIsCancelled = false;
private OutputStream mOutStream;
private File mTmpFile;
/**
* {@inheritDoc}
*/
public void addOutput(byte[] data, int offset, int length) {
try {
mOutStream.write(data, offset, length);
} catch (IOException e) {
Log.w(LOG_TAG, String.format(
"failed to write logcat data for %s.", getSerialNumber()));
}
}
public InputStream getLogcatData() {
flush();
try {
FileInputStream fileStream = new FileInputStream(mTmpFile);
// adjust stream position to so only up to LOGCAT_MAX_DATA bytes can be read
if (mTmpFile.length() > LOGCAT_MAX_DATA) {
fileStream.skip(mTmpFile.length() - LOGCAT_MAX_DATA);
}
return fileStream;
} catch (IOException e) {
Log.e(LOG_TAG, String.format(
"failed to get logcat data for %s.", getSerialNumber()));
Log.e(LOG_TAG, e);
// return an empty input stream
return new ByteArrayInputStream(new byte[0]);
}
}
/**
* {@inheritDoc}
*/
public void flush() {
try {
mOutStream.flush();
} catch (IOException e) {
Log.w(LOG_TAG, String.format(
"failed to flush logcat data for %s.", getSerialNumber()));
}
}
public void cancel() {
mIsCancelled = true;
interrupt();
try {
if (mOutStream != null) {
mOutStream.close();
mOutStream = null;
}
} catch (IOException e) {
Log.w(LOG_TAG, String.format(
"failed to close logcat stream for %s.", getSerialNumber()));
}
if (mTmpFile != null) {
mTmpFile.delete();
mTmpFile = null;
}
}
/**
* {@inheritDoc}
*/
public boolean isCancelled() {
return mIsCancelled;
}
@Override
public void run() {
try {
mTmpFile = File.createTempFile(String.format("logcat_%s_", getSerialNumber()),
".txt");
Log.i(LOG_TAG, String.format("Created tmp logcat file %s",
mTmpFile.getAbsolutePath()));
mOutStream = new BufferedOutputStream(new FileOutputStream(mTmpFile),
LOGCAT_BUFF_SIZE);
} catch (IOException e) {
Log.w(LOG_TAG, String.format(
"failed to create tmp logcat file for %s.", getSerialNumber()));
return;
}
// continually run in a loop attempting to grab logcat data, skipping recovery
// this is done so logcat data can continue to be captured even if device goes away and
// then comes back online
while (!isCancelled()) {
try {
Log.d(LOG_TAG, String.format("Starting logcat for %s.", getSerialNumber()));
getIDevice().executeShellCommand(LOGCAT_CMD, this);
} catch (IOException e) {
final String msg = String.format(
"logcat capture interrupted for %s. Sleeping for %d ms. May see " +
"duplicate content in log.",
getSerialNumber(), LOGCAT_SLEEP_TIME);
Log.d(LOG_TAG, msg);
appendDeviceLogMsg(msg);
}
try {
sleep(LOGCAT_SLEEP_TIME);
} catch (InterruptedException e) {
// ignore
}
}
}
/**
* Adds a message to the captured device log.
* @param msg
*/
private void appendDeviceLogMsg(String msg) {
// add the msg to device tmp log, so readers will know logcat was interrupted
try {
mOutStream.write("\n*******************\n".getBytes());
mOutStream.write(msg.getBytes());
mOutStream.write("\n*******************\n".getBytes());
} catch (IOException e) {
Log.w(LOG_TAG, String.format(
"failed to write logcat data for %s.", getSerialNumber()));
}
}
}
IDeviceStateMonitor getDeviceStateMonitor() {
return mMonitor;
}
/**
* Perform instructions to configure device for testing that must be done after every boot.
* <p/>
* TODO: move this to another configurable interface
*
* @throws DeviceNotAvailableException if connection with device is lost and cannot be
* recovered.
*/
public void postBootSetup() throws DeviceNotAvailableException {
if (mEnableAdbRoot) {
enableAdbRoot();
}
// turn off keyguard
executeShellCommand("input keyevent 82");
}
/**
* {@inheritDoc}
*/
public void rebootIntoBootloader() throws DeviceNotAvailableException {
if (TestDeviceState.FASTBOOT == mMonitor.getDeviceState()) {
Log.i(LOG_TAG, String.format("device %s already in fastboot. Rebooting anyway",
getSerialNumber()));
executeFastbootCommand("reboot", "bootloader");
} else {
Log.i(LOG_TAG, String.format("Booting device %s into bootloader",
getSerialNumber()));
doAdbReboot("bootloader");
}
if (!mMonitor.waitForDeviceBootloader(FASTBOOT_TIMEOUT)) {
// TODO: add recoverBootloader method
mRecovery.recoverDeviceBootloader(getIDevice(), mMonitor);
}
// TODO: check for fastboot responsiveness ?
}
/**
* {@inheritDoc}
*/
public void reboot() throws DeviceNotAvailableException {
if (TestDeviceState.FASTBOOT == mMonitor.getDeviceState()) {
Log.i(LOG_TAG, String.format("device %s in fastboot. Rebooting to userspace.",
getSerialNumber()));
executeFastbootCommand("reboot");
} else {
Log.i(LOG_TAG, String.format("Rebooting device %s", getSerialNumber()));
doAdbReboot(null);
waitForDeviceNotAvailable("reboot", 2*60*1000);
}
if (!mMonitor.waitForDeviceAvailable()) {
recoverDevice();
}
postBootSetup();
}
/**
* Perform a adb reboot.
*
* @param into the bootloader name to reboot into, or <code>null</code> to just reboot the
* device.
* @throws DeviceNotAvailableException
*/
private void doAdbReboot(final String into) throws DeviceNotAvailableException {
IDeviceAction rebootAction = new IDeviceAction() {
public boolean doAction() throws IOException {
getIDevice().reboot(into);
return true;
}
};
performDeviceAction("reboot", rebootAction, MAX_RETRY_ATTEMPTS);
}
private void waitForDeviceNotAvailable(String operationDesc, long time) {
// TODO: a bit of a race condition here. Would be better to start a device listener
// before the operation
if (!mMonitor.waitForDeviceNotAvailable(time)) {
// above check is flaky, ignore till better solution is found
Log.w(LOG_TAG, String.format(
"Did not detect device %s becoming unavailable after %s",
getSerialNumber(), operationDesc));
}
}
/**
* {@inheritDoc}
*/
public boolean enableAdbRoot() throws DeviceNotAvailableException {
Log.i(LOG_TAG, String.format("adb root on device %s", getSerialNumber()));
String output = executeAdbCommand("root");
boolean success = (output.contains("restarting adbd as root") ||
output.contains("adbd is already running as root"));
if (!success) {
return false;
}
// wait for device to disappear from adb
waitForDeviceNotAvailable("root", 20*1000);
// wait for device to be back online
waitForDeviceAvailable(ROOT_TIMEOUT);
return true;
}
/**
* {@inheritDoc}
*/
public void waitForDeviceOnline() throws DeviceNotAvailableException {
if (!mMonitor.waitForDeviceOnline()) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
public void waitForDeviceAvailable(long waitTime) throws DeviceNotAvailableException {
if (!mMonitor.waitForDeviceAvailable(waitTime)) {
recoverDevice();
}
}
/**
* {@inheritDoc}
*/
public void waitForDeviceAvailable() throws DeviceNotAvailableException {
if (!mMonitor.waitForDeviceAvailable()) {
recoverDevice();
}
}
void setEnableAdbRoot(boolean enable) {
mEnableAdbRoot = enable;
}
}