blob: ae20920873d1f28af57f4a6212f69adb92670c46 [file] [log] [blame]
/*
* Copyright (C) 2019 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.Log.LogLevel;
import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.cloud.GceAvdInfo;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestLoggerReceiver;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.TarUtil;
import com.android.tradefed.util.ZipUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.net.HostAndPort;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** The class for local virtual devices running on TradeFed host. */
public class LocalAndroidVirtualDevice extends RemoteAndroidDevice implements ITestLoggerReceiver {
private static final int INVALID_PORT = 0;
// Environment variables.
private static final String ANDROID_HOST_OUT = "ANDROID_HOST_OUT";
private static final String TMPDIR = "TMPDIR";
// The name of the GZIP file containing launch_cvd and stop_cvd.
private static final String CVD_HOST_PACKAGE_NAME = "cvd-host_package.tar.gz";
private static final String ACLOUD_CVD_TEMP_DIR_NAME = "acloud_cvd_temp";
private static final String CUTTLEFISH_RUNTIME_DIR_NAME = "cuttlefish_runtime";
private ITestLogger mTestLogger = null;
// Temporary directories for images and tools.
private File mImageDir = null;
private File mHostPackageDir = null;
private List<File> mTempDirs = new ArrayList<File>();
private GceAvdInfo mGceAvdInfo = null;
public LocalAndroidVirtualDevice(
IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) {
super(device, stateMonitor, allocationMonitor);
}
/** Execute common setup procedure and launch the virtual device. */
@Override
public void preInvocationSetup(IBuildInfo info)
throws TargetSetupError, DeviceNotAvailableException {
// The setup method in super class does not require the device to be online.
super.preInvocationSetup(info);
createTempDirs(info);
CommandResult result = null;
File report = null;
try {
report = FileUtil.createTempFile("report", ".json");
result = acloudCreate(report, getOptions());
loadAvdInfo(report);
} catch (IOException ex) {
throw new TargetSetupError(
"Cannot create acloud report file.", ex, getDeviceDescriptor());
} finally {
FileUtil.deleteFile(report);
}
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
throw new TargetSetupError(
String.format("Cannot execute acloud command. stderr:\n%s", result.getStderr()),
getDeviceDescriptor());
}
HostAndPort hostAndPort = mGceAvdInfo.hostAndPort();
replaceStubDevice(hostAndPort.toString());
RecoveryMode previousMode = getRecoveryMode();
try {
setRecoveryMode(RecoveryMode.NONE);
if (!adbTcpConnect(hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) {
throw new TargetSetupError(
String.format("Cannot connect to %s.", hostAndPort), getDeviceDescriptor());
}
waitForDeviceAvailable();
} finally {
setRecoveryMode(previousMode);
}
}
/** Execute common tear-down procedure and stop the virtual device. */
@Override
public void postInvocationTearDown(Throwable exception) {
TestDeviceOptions options = getOptions();
HostAndPort hostAndPort = getHostAndPortFromAvdInfo();
String instanceName = (mGceAvdInfo != null ? mGceAvdInfo.instanceName() : null);
try {
if (!options.shouldSkipTearDown() && hostAndPort != null) {
if (!adbTcpDisconnect(
hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) {
CLog.e("Cannot disconnect from %s", hostAndPort.toString());
}
}
if (!options.shouldSkipTearDown() && instanceName != null) {
CommandResult result = acloudDelete(instanceName, options);
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
CLog.e("Cannot stop the virtual device.");
}
} else {
CLog.i("Skip stopping the virtual device.");
}
if (instanceName != null) {
reportInstanceLogs(instanceName);
}
} finally {
restoreStubDevice();
if (!options.shouldSkipTearDown()) {
deleteTempDirs();
} else {
CLog.i(
"Skip deleting the temporary directories.\n"
+ "Address: %s\nName: %s\nHost package: %s\nImage: %s",
hostAndPort, instanceName, mHostPackageDir, mImageDir);
mTempDirs.clear();
mHostPackageDir = null;
mImageDir = null;
}
mGceAvdInfo = null;
super.postInvocationTearDown(exception);
}
}
@Override
public void setTestLogger(ITestLogger testLogger) {
mTestLogger = testLogger;
}
/**
* Extract a file if the format is tar.gz or zip.
*
* @param file the file to be extracted.
* @return a temporary directory containing the extracted content if the file is an archive;
* otherwise return the input file.
* @throws IOException if the file cannot be extracted.
*/
private File extractArchive(File file) throws IOException {
if (file.isDirectory()) {
return file;
}
if (TarUtil.isGzip(file)) {
file = TarUtil.extractTarGzipToTemp(file, file.getName());
mTempDirs.add(file);
} else if (ZipUtil.isZipFileValid(file, false)) {
file = ZipUtil.extractZipToTemp(file, file.getName());
mTempDirs.add(file);
} else {
CLog.w("Cannot extract %s.", file);
}
return file;
}
/** Find host package in build info and extract to a temporary directory. */
private File findHostPackage(IBuildInfo buildInfo) throws TargetSetupError {
File hostPackageDir = null;
File hostPackage = buildInfo.getFile(CVD_HOST_PACKAGE_NAME);
if (hostPackage != null) {
try {
hostPackageDir = extractArchive(hostPackage);
} catch (IOException ex) {
throw new TargetSetupError(
"Cannot extract host package.", ex, getDeviceDescriptor());
}
}
if (hostPackageDir == null) {
String androidHostOut = System.getenv(ANDROID_HOST_OUT);
if (!Strings.isNullOrEmpty(androidHostOut)) {
CLog.i(
"Use the host tools in %s as the build info does not provide host package.",
androidHostOut);
hostPackageDir = new File(androidHostOut);
}
}
if (hostPackageDir == null || !hostPackageDir.isDirectory()) {
throw new TargetSetupError(
String.format(
"Cannot find %s in build info and %s.",
CVD_HOST_PACKAGE_NAME, ANDROID_HOST_OUT),
getDeviceDescriptor());
}
FileUtil.chmodRWXRecursively(new File(hostPackageDir, "bin"));
return hostPackageDir;
}
/** Find device images in build info and extract to a temporary directory. */
private File findDeviceImages(IBuildInfo buildInfo) throws TargetSetupError {
File imageZip = buildInfo.getFile(BuildInfoFileKey.DEVICE_IMAGE);
if (imageZip == null) {
throw new TargetSetupError(
"Cannot find image zip in build info.", getDeviceDescriptor());
}
try {
return extractArchive(imageZip);
} catch (IOException ex) {
throw new TargetSetupError("Cannot extract image zip.", ex, getDeviceDescriptor());
}
}
/** Get the necessary files to create the instance. */
void createTempDirs(IBuildInfo info) throws TargetSetupError {
try {
mHostPackageDir = findHostPackage(info);
mImageDir = findDeviceImages(info);
} catch (TargetSetupError ex) {
deleteTempDirs();
throw ex;
}
}
/** Delete all temporary directories. */
@VisibleForTesting
void deleteTempDirs() {
for (File tempDir : mTempDirs) {
FileUtil.recursiveDelete(tempDir);
}
mTempDirs.clear();
mImageDir = null;
mHostPackageDir = null;
}
/**
* Change the initial serial number of {@link StubLocalAndroidVirtualDevice}.
*
* @param newSerialNumber the serial number of the new stub device.
* @throws TargetSetupError if the original device type is not expected.
*/
private void replaceStubDevice(String newSerialNumber) throws TargetSetupError {
IDevice device = getIDevice();
if (!StubLocalAndroidVirtualDevice.class.equals(device.getClass())) {
throw new TargetSetupError(
"Unexpected device type: " + device.getClass(), getDeviceDescriptor());
}
setIDevice(new StubLocalAndroidVirtualDevice(newSerialNumber));
setFastbootEnabled(false);
}
/** Restore the {@link StubLocalAndroidVirtualDevice} with the initial serial number. */
private void restoreStubDevice() {
setIDevice(new StubLocalAndroidVirtualDevice(getInitialSerial()));
setFastbootEnabled(false);
}
private static void addLogLevelToAcloudCommand(List<String> command, LogLevel logLevel) {
if (LogLevel.VERBOSE.equals(logLevel)) {
command.add("-v");
} else if (LogLevel.DEBUG.equals(logLevel)) {
command.add("-vv");
}
}
private CommandResult acloudCreate(File report, TestDeviceOptions options) {
CommandResult result = null;
File acloud = options.getAvdDriverBinary();
if (acloud == null || !acloud.isFile()) {
CLog.e("Specified AVD driver binary is not a file.");
result = new CommandResult(CommandStatus.EXCEPTION);
result.setStderr("Specified AVD driver binary is not a file.");
return result;
}
acloud.setExecutable(true);
for (int attempt = 0; attempt < options.getGceMaxAttempt(); attempt++) {
result =
acloudCreate(
options.getGceCmdTimeout(),
acloud,
report,
options.getGceDriverLogLevel(),
options.getGceDriverParams());
if (CommandStatus.SUCCESS.equals(result.getStatus())) {
break;
}
CLog.w(
"Failed to start local virtual instance with attempt: %d; command status: %s",
attempt, result.getStatus());
}
return result;
}
private CommandResult acloudCreate(
long timeout,
File acloud,
File report,
LogLevel logLevel,
List<String> args) {
IRunUtil runUtil = createRunUtil();
// The command creates the instance directory under TMPDIR.
runUtil.setEnvVariable(TMPDIR, getTmpDir().getAbsolutePath());
List<String> command =
new ArrayList<String>(
Arrays.asList(
acloud.getAbsolutePath(),
"create",
"--local-instance",
"--local-image",
mImageDir.getAbsolutePath(),
"--local-tool",
mHostPackageDir.getAbsolutePath(),
"--report_file",
report.getAbsolutePath(),
"--no-autoconnect",
"--yes",
"--skip-pre-run-check"));
addLogLevelToAcloudCommand(command, logLevel);
command.addAll(args);
CommandResult result = runUtil.runTimedCmd(timeout, command.toArray(new String[0]));
CLog.i("acloud create stdout:\n%s", result.getStdout());
CLog.i("acloud create stderr:\n%s", result.getStderr());
return result;
}
/**
* Get valid host and port from mGceAvdInfo.
*
* @return {@link HostAndPort} if the port is valid; null otherwise.
*/
private HostAndPort getHostAndPortFromAvdInfo() {
if (mGceAvdInfo == null) {
return null;
}
HostAndPort hostAndPort = mGceAvdInfo.hostAndPort();
if (hostAndPort == null
|| !hostAndPort.hasPort()
|| hostAndPort.getPort() == INVALID_PORT) {
return null;
}
return hostAndPort;
}
/** Initialize instance name, host address, and port from an acloud report file. */
private void loadAvdInfo(File report) throws TargetSetupError {
mGceAvdInfo = GceAvdInfo.parseGceInfoFromFile(report, getDeviceDescriptor(), INVALID_PORT);
if (mGceAvdInfo == null) {
throw new TargetSetupError("Cannot read acloud report file.", getDeviceDescriptor());
}
if (Strings.isNullOrEmpty(mGceAvdInfo.instanceName())) {
throw new TargetSetupError("No instance name in acloud report.", getDeviceDescriptor());
}
if (getHostAndPortFromAvdInfo() == null) {
throw new TargetSetupError("No port in acloud report.", getDeviceDescriptor());
}
if (!GceAvdInfo.GceStatus.SUCCESS.equals(mGceAvdInfo.getStatus())) {
throw new TargetSetupError(
"Cannot launch virtual device: " + mGceAvdInfo.getErrors(),
getDeviceDescriptor());
}
}
private CommandResult acloudDelete(String instanceName, TestDeviceOptions options) {
File acloud = options.getAvdDriverBinary();
if (acloud == null || !acloud.isFile()) {
CLog.e("Specified AVD driver binary is not a file.");
return new CommandResult(CommandStatus.EXCEPTION);
}
acloud.setExecutable(true);
IRunUtil runUtil = createRunUtil();
runUtil.setEnvVariable(TMPDIR, getTmpDir().getAbsolutePath());
List<String> command =
new ArrayList<String>(
Arrays.asList(
acloud.getAbsolutePath(),
"delete",
"--local-only",
"--instance-names",
instanceName));
addLogLevelToAcloudCommand(command, options.getGceDriverLogLevel());
CommandResult result =
runUtil.runTimedCmd(options.getGceCmdTimeout(), command.toArray(new String[0]));
CLog.i("acloud delete stdout:\n%s", result.getStdout());
CLog.i("acloud delete stderr:\n%s", result.getStderr());
return result;
}
private void reportInstanceLogs(String instanceName) {
if (mTestLogger == null) {
return;
}
File instanceDir =
FileUtil.getFileForPath(
getTmpDir(),
ACLOUD_CVD_TEMP_DIR_NAME,
instanceName,
CUTTLEFISH_RUNTIME_DIR_NAME);
reportInstanceLog(new File(instanceDir, "kernel.log"), LogDataType.KERNEL_LOG);
reportInstanceLog(new File(instanceDir, "logcat"), LogDataType.LOGCAT);
reportInstanceLog(new File(instanceDir, "launcher.log"), LogDataType.TEXT);
reportInstanceLog(new File(instanceDir, "cuttlefish_config.json"), LogDataType.TEXT);
}
private void reportInstanceLog(File file, LogDataType type) {
if (file.exists()) {
try (InputStreamSource source = new FileInputStreamSource(file)) {
mTestLogger.testLog(file.getName(), type, source);
}
} else {
CLog.w("%s doesn't exist.", file.getAbsolutePath());
}
}
@VisibleForTesting
IRunUtil createRunUtil() {
return new RunUtil();
}
@VisibleForTesting
File getTmpDir() {
return new File(System.getProperty("java.io.tmpdir"));
}
}