Home | History | Annotate | Download | only in targetprep
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tradefed.targetprep;
     18 
     19 import com.android.ddmlib.EmulatorConsole;
     20 import com.android.tradefed.build.IBuildInfo;
     21 import com.android.tradefed.build.ISdkBuildInfo;
     22 import com.android.tradefed.config.GlobalConfiguration;
     23 import com.android.tradefed.config.Option;
     24 import com.android.tradefed.device.DeviceNotAvailableException;
     25 import com.android.tradefed.device.IDeviceManager;
     26 import com.android.tradefed.device.ITestDevice;
     27 import com.android.tradefed.device.TestDeviceState;
     28 import com.android.tradefed.log.LogUtil.CLog;
     29 import com.android.tradefed.util.ArrayUtil;
     30 import com.android.tradefed.util.CommandResult;
     31 import com.android.tradefed.util.CommandStatus;
     32 import com.android.tradefed.util.FileUtil;
     33 import com.android.tradefed.util.IRunUtil;
     34 import com.android.tradefed.util.RunUtil;
     35 
     36 import com.google.common.annotations.VisibleForTesting;
     37 
     38 import org.junit.Assert;
     39 
     40 import java.io.File;
     41 import java.io.IOException;
     42 import java.util.ArrayList;
     43 import java.util.Collection;
     44 import java.util.HashMap;
     45 import java.util.List;
     46 import java.util.Map;
     47 
     48 /**
     49  * A {@link ITargetPreparer} that will create an avd and launch an emulator
     50  */
     51 public class SdkAvdPreparer implements ITargetPreparer, IHostCleaner {
     52 
     53 
     54     @Option(name = "sdk-target", description = "the name of SDK target to launch. " +
     55             "If unspecified, will use first target found")
     56     private String mTargetName = null;
     57 
     58     @Option(name = "boot-time", description =
     59         "the maximum time in minutes to wait for emulator to boot.")
     60     private long mMaxBootTime = 5;
     61 
     62     @Option(name = "window", description = "launch emulator with a graphical window display.")
     63     private boolean mWindow = false;
     64 
     65     @Option(name = "launch-attempts", description = "max number of attempts to launch emulator")
     66     private int mLaunchAttempts = 1;
     67 
     68     @Option(name = "sdcard-size", description = "capacity of the SD card")
     69     private String mSdcardSize = "10M";
     70 
     71     @Option(name = "tag", description = "The sys-img tag to use for the AVD.")
     72     private String mAvdTag = null;
     73 
     74     @Option(name = "skin", description = "AVD skin")
     75     private String mAvdSkin = null;
     76 
     77     @Option(name = "gpu", description = "launch emulator with GPU on")
     78     private boolean mGpu = false;
     79 
     80     @Option(name = "force-kvm", description = "require kvm for emulator launch")
     81     private boolean mForceKvm = false;
     82 
     83     @Option(name = "avd-timeout", description = "the maximum time in seconds to wait for avd " +
     84             "creation")
     85     private int mAvdTimeoutSeconds = 30;
     86 
     87     @Option(name = "emulator-device-type", description = "emulator device type to launch." +
     88             "If unspecified, will launch generic version")
     89     private String mDevice = null;
     90 
     91     @Option(name = "display", description = "which display to launch the emulator in. " +
     92             "If unspecified, display will not be set. Display values should start with :" +
     93             " for example for display 1 use ':1'.")
     94     private String mDisplay = null;
     95 
     96     @Option(name = "abi", description = "abi to select for the avd")
     97     private String mAbi = null;
     98 
     99     @Option(name = "emulator-system-image",
    100             description = "system image will be loaded into emulator.")
    101     private String mEmulatorSystemImage = null;
    102 
    103     @Option(name = "emulator-ramdisk-image",
    104             description = "ramdisk image will be loaded into emulator.")
    105     private String mEmulatorRamdiskImage = null;
    106 
    107     @Option(name = "prop", description = "pass key-value pairs of system props")
    108     private Map<String,String> mProps = new HashMap<String, String>();
    109 
    110     @Option(name = "hw-options", description = "pass key-value pairs of avd hardware options")
    111     private Map<String,String> mHwOptions = new HashMap<String, String>();
    112 
    113     @Option(name = "emulator-binary", description = "location of the emulator binary")
    114     private String mEmulatorBinary = null;
    115 
    116     @Option(name = "emulator-arg",
    117             description = "Additional argument to launch the emulator with. Can be repeated.")
    118     private Collection<String> mEmulatorArgs = new ArrayList<String>();
    119 
    120     @Option(name = "verbose", description = "Use verbose for emulator output")
    121     private boolean mVerbose = false;
    122 
    123     private final IRunUtil mRunUtil;
    124     private IDeviceManager mDeviceManager;
    125     private ITestDevice mTestDevice;
    126 
    127     private File mSdkHome = null;
    128 
    129     /**
    130      * Creates a {@link SdkAvdPreparer}.
    131      */
    132     public SdkAvdPreparer() {
    133         this(new RunUtil(), null);
    134     }
    135 
    136     /**
    137      * Alternate constructor for injecting dependencies.
    138      *
    139      * @param runUtil
    140      */
    141     SdkAvdPreparer(IRunUtil runUtil, IDeviceManager deviceManager) {
    142         mRunUtil = runUtil;
    143         mDeviceManager = deviceManager;
    144     }
    145 
    146 
    147     /**
    148      * {@inheritDoc}
    149      */
    150     @Override
    151     public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
    152             DeviceNotAvailableException, BuildError {
    153         Assert.assertTrue("Provided build is not a ISdkBuildInfo",
    154                 buildInfo instanceof ISdkBuildInfo);
    155         mTestDevice = device;
    156         ISdkBuildInfo sdkBuildInfo = (ISdkBuildInfo)buildInfo;
    157         launchEmulatorForAvd(sdkBuildInfo, device, createAvd(sdkBuildInfo));
    158     }
    159 
    160     /**
    161      * Finds SDK target based on the {@link ISdkBuildInfo}, creates AVD for
    162      * this target and returns its name.
    163      *
    164      * @param sdkBuildInfo the {@link ISdkBuildInfo}
    165      * @return the created AVD name
    166      * @throws TargetSetupError if could not get targets
    167      * @throws BuildError if failed to create the AVD
    168      */
    169     public String createAvd(ISdkBuildInfo sdkBuildInfo)
    170           throws TargetSetupError, BuildError {
    171         String[] targets = getSdkTargets(sdkBuildInfo);
    172         setAndroidSdkHome();
    173         String target = findTargetToLaunch(targets);
    174         return createAvdForTarget(sdkBuildInfo, target);
    175     }
    176 
    177     /**
    178      * Launch an emulator for given avd, and wait for it to become available.
    179      * Will launch the emulator on the port specified in the allocated {@link ITestDevice}
    180      *
    181      * @param sdkBuild the {@link ISdkBuildInfo}
    182      * @param device the placeholder {@link ITestDevice} representing allocated emulator device
    183      * @param avd the avd to launch
    184      * @throws DeviceNotAvailableException
    185      * @throws TargetSetupError if could not get targets
    186      * @throws BuildError if emulator fails to boot
    187      */
    188     public void launchEmulatorForAvd(ISdkBuildInfo sdkBuild, ITestDevice device, String avd)
    189             throws DeviceNotAvailableException, TargetSetupError, BuildError {
    190         if (!device.getDeviceState().equals(TestDeviceState.NOT_AVAILABLE)) {
    191             CLog.w("Emulator %s is already running, killing", device.getSerialNumber());
    192             getDeviceManager().killEmulator(device);
    193         } else if (!device.getIDevice().isEmulator()) {
    194             throw new TargetSetupError("Invalid stub device, it is not of type emulator",
    195                     device.getDeviceDescriptor());
    196         }
    197 
    198         mRunUtil.setEnvVariable("ANDROID_SDK_ROOT", sdkBuild.getSdkDir().getAbsolutePath());
    199 
    200         String emulatorBinary =
    201             mEmulatorBinary == null ? sdkBuild.getEmulatorToolPath() : mEmulatorBinary;
    202         List<String> emulatorArgs = ArrayUtil.list(emulatorBinary, "-avd", avd);
    203 
    204         if (mDisplay != null) {
    205             emulatorArgs.add(0, "DISPLAY=" + mDisplay);
    206         }
    207         // Ensure the emulator will launch on the same port as the allocated emulator device
    208         Integer port = EmulatorConsole.getEmulatorPort(device.getSerialNumber());
    209         if (port == null) {
    210             // Serial number is not in expected format <type>-<consolePort> as defined by ddmlib
    211             throw new TargetSetupError(String.format(
    212                     "Failed to determine emulator port for %s", device.getSerialNumber()),
    213                     device.getDeviceDescriptor());
    214         }
    215         emulatorArgs.add("-port");
    216         emulatorArgs.add(port.toString());
    217 
    218         if (!mWindow) {
    219             emulatorArgs.add("-no-window");
    220             emulatorArgs.add("-no-audio");
    221         }
    222 
    223         if (mGpu) {
    224             emulatorArgs.add("-gpu");
    225             emulatorArgs.add("on");
    226         }
    227 
    228         if (mVerbose) {
    229             emulatorArgs.add("-verbose");
    230         }
    231 
    232         for (Map.Entry<String, String> propEntry : mProps.entrySet()) {
    233             emulatorArgs.add("-prop");
    234             emulatorArgs.add(String.format("%s=%s", propEntry.getKey(), propEntry.getValue()));
    235         }
    236         for (String arg : mEmulatorArgs) {
    237             String[] tokens = arg.split(" ");
    238             if (tokens.length == 1 && tokens[0].startsWith("-")) {
    239                 emulatorArgs.add(tokens[0]);
    240             } else if (tokens.length == 2) {
    241                 if (!tokens[0].startsWith("-")) {
    242                     throw new TargetSetupError(String.format("The emulator arg '%s' is invalid.",
    243                             arg), device.getDeviceDescriptor());
    244                 }
    245                 emulatorArgs.add(tokens[0]);
    246                 emulatorArgs.add(tokens[1]);
    247             } else {
    248                 throw new TargetSetupError(String.format(
    249                         "The emulator arg '%s' is invalid.", arg), device.getDeviceDescriptor());
    250             }
    251         }
    252 
    253         setCommandList(emulatorArgs, "-system", mEmulatorSystemImage);
    254         setCommandList(emulatorArgs, "-ramdisk", mEmulatorRamdiskImage);
    255 
    256         // qemu must be the last parameter, it assumes params that follow it are it's own
    257         if(mForceKvm) {
    258             emulatorArgs.add("-qemu");
    259             emulatorArgs.add("-enable-kvm");
    260         }
    261 
    262         launchEmulator(device, avd, emulatorArgs);
    263         if (!avd.equals(getAvdNameFromEmulator(device))) {
    264             // not good. Either emulator isn't reporting its avd name properly, or somehow
    265             // the wrong emulator launched. Treat as a BuildError
    266             throw new BuildError(String.format(
    267                     "Emulator booted with incorrect avd name '%s'. Expected: '%s'.",
    268                     device.getIDevice().getAvdName(), avd), device.getDeviceDescriptor());
    269         }
    270     }
    271 
    272     String getAvdNameFromEmulator(ITestDevice device) {
    273         String avdName = device.getIDevice().getAvdName();
    274         if (avdName == null) {
    275             CLog.w("IDevice#getAvdName is null");
    276             // avdName is set asynchronously on startup, which explains why it might be null
    277             // query directly as work around
    278             EmulatorConsole console = EmulatorConsole.getConsole(device.getIDevice());
    279             if (console != null) {
    280                 avdName = console.getAvdName();
    281             }
    282         }
    283         return avdName;
    284     }
    285 
    286     /**
    287      * Sets programmatically whether the gpu should be on or off.
    288      *
    289      * @param gpu
    290      */
    291     public void setGpu(boolean gpu) {
    292         mGpu = gpu;
    293     }
    294 
    295     public void setForceKvm(boolean forceKvm) {
    296         mForceKvm = forceKvm;
    297     }
    298 
    299     /**
    300      * Gets the list of sdk targets from the given sdk.
    301      *
    302      * @param sdkBuild
    303      * @return a list of defined targets
    304      * @throws TargetSetupError if could not get targets
    305      */
    306     private String[] getSdkTargets(ISdkBuildInfo sdkBuild) throws TargetSetupError {
    307         // Need to set the ANDROID_SWT environment variable needed by android tool.
    308         mRunUtil.setEnvVariable("ANDROID_SWT", getSWTDirPath(sdkBuild));
    309         CommandResult result = mRunUtil.runTimedCmd(getAvdTimeoutMS(),
    310                 sdkBuild.getAndroidToolPath(), "list", "targets", "--compact");
    311         if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
    312             throw new TargetSetupError(String.format(
    313                     "Unable to get list of SDK targets using %s. Result %s. stdout: %s, err: %s",
    314                     sdkBuild.getAndroidToolPath(), result.getStatus(), result.getStdout(),
    315                     result.getStderr()), mTestDevice.getDeviceDescriptor());
    316         }
    317         String[] targets = result.getStdout().split("\n");
    318         if (result.getStdout().trim().isEmpty() || targets.length == 0) {
    319             throw new TargetSetupError(String.format("No targets found in SDK %s.",
    320                     sdkBuild.getSdkDir().getAbsolutePath()), mTestDevice.getDeviceDescriptor());
    321         }
    322         return targets;
    323     }
    324 
    325     private String getSWTDirPath(ISdkBuildInfo sdkBuild) {
    326         return FileUtil.getPath(sdkBuild.getSdkDir().getAbsolutePath(), "tools", "lib");
    327     }
    328 
    329     /**
    330      * Sets the ANDROID_SDK_HOME environment variable. The SDK home directory is used as the
    331      * location for SDK file storage of AVD definition files, etc.
    332      */
    333     private void setAndroidSdkHome() throws TargetSetupError {
    334         try {
    335             // if necessary, create a dir to group the tmp sdk homes
    336             File tmpParent = createParentSdkHome();
    337             // create a temp dir inside the grouping folder
    338             mSdkHome = FileUtil.createTempDir("SDK_home", tmpParent);
    339             // store avds etc in tmp location, and clean up on teardown
    340             mRunUtil.setEnvVariable("ANDROID_SDK_HOME", mSdkHome.getAbsolutePath());
    341         } catch (IOException e) {
    342             throw new TargetSetupError("Failed to create sdk home",
    343                     mTestDevice.getDeviceDescriptor());
    344         }
    345     }
    346 
    347     /**
    348      * Create the parent directory where SDK_home will be stored.
    349      */
    350     @VisibleForTesting
    351     File createParentSdkHome() throws IOException {
    352         return FileUtil.createNamedTempDir("SDK_homes");
    353     }
    354 
    355     /**
    356      * Find the SDK target to use.
    357      * <p/>IOException
    358      * Will use the 'sdk-target' option if specified, otherwise will return last target in target
    359      * list.
    360      *
    361      * @param targets the list of targets in SDK
    362      * @return the SDK target name
    363      * @throws TargetSetupError if specified 'sdk-target' cannot be found
    364      */
    365     private String findTargetToLaunch(String[] targets) throws TargetSetupError {
    366         if (mTargetName != null) {
    367             for (String foundTarget : targets) {
    368                 if (foundTarget.equals(mTargetName)) {
    369                     return mTargetName;
    370                 }
    371             }
    372             throw new TargetSetupError(String.format("Could not find target %s in sdk",
    373                     mTargetName), mTestDevice.getDeviceDescriptor());
    374         }
    375         // just return last target
    376         return targets[targets.length - 1];
    377     }
    378 
    379     /**
    380      * Create an AVD for given SDK target.
    381      *
    382      * @param sdkBuild the {@link ISdkBuildInfo}
    383      * @param target the SDK target name
    384      * @return the created AVD name
    385      * @throws BuildError if failed to create the AVD
    386      *
    387      */
    388     private String createAvdForTarget(ISdkBuildInfo sdkBuild, String target)
    389             throws BuildError, TargetSetupError {
    390         // answer 'no' when prompted for creating a custom hardware profile
    391         final String cmdInput = "no\r\n";
    392         final String targetName = createAvdName(target);
    393         final String successPattern = String.format("Created AVD '%s'", targetName);
    394         CLog.d("Creating avd for target %s with name %s", target, targetName);
    395 
    396         List<String> avdCommand = ArrayUtil.list(sdkBuild.getAndroidToolPath(), "create", "avd");
    397 
    398         setCommandList(avdCommand, "--abi", mAbi);
    399         setCommandList(avdCommand, "--device", mDevice);
    400         setCommandList(avdCommand, "--sdcard", mSdcardSize);
    401         setCommandList(avdCommand, "--target", target);
    402         setCommandList(avdCommand, "--name", targetName);
    403         setCommandList(avdCommand, "--tag", mAvdTag);
    404         setCommandList(avdCommand, "--skin", mAvdSkin);
    405         avdCommand.add("--force");
    406 
    407         CommandResult result = mRunUtil.runTimedCmdWithInput(getAvdTimeoutMS(),
    408               cmdInput, avdCommand);
    409         if (!result.getStatus().equals(CommandStatus.SUCCESS) || result.getStdout() == null ||
    410                 !result.getStdout().contains(successPattern)) {
    411             // stdout usually doesn't contain useful data, so don't want to add it to the
    412             // exception message. However, log it here as a debug log so the info is captured
    413             // in log
    414             CLog.d("AVD creation failed. status: '%s' stdout: '%s'", result.getStatus(),
    415                     result.getStdout());
    416             // treat as BuildError
    417             throw new BuildError(String.format(
    418                     "Unable to create avd for target '%s'. stderr: '%s'", target,
    419                     result.getStderr()), mTestDevice.getDeviceDescriptor());
    420         }
    421 
    422         // Further customise hardware options after AVD was created
    423         if (!mHwOptions.isEmpty()) {
    424             addHardwareOptions();
    425         }
    426 
    427         return targetName;
    428     }
    429 
    430     // Create a valid AVD name, by removing invalid characters from target name.
    431     private String createAvdName(String target) {
    432         if (target == null)  {
    433             return null;
    434         }
    435         return target.replaceAll("[^a-zA-Z0-9\\.\\-]", "");
    436     }
    437 
    438     // Overwrite or add AVD hardware options by appending them to the config file used by the AVD
    439     private void addHardwareOptions() throws TargetSetupError {
    440         if (mHwOptions.isEmpty()) {
    441             CLog.d("No hardware options to add");
    442             return;
    443         }
    444 
    445         // config.ini file contains all the hardware options loaded on the AVD
    446         final String configFileName = "config.ini";
    447         File configFile = FileUtil.findFile(mSdkHome, configFileName);
    448         if (configFile == null) {
    449             // Shouldn't happened if AVD was created successfully
    450             throw new RuntimeException("Failed to find " + configFileName);
    451         }
    452 
    453         for (Map.Entry<String, String> hwOption : mHwOptions.entrySet()) {
    454             // if the config file contain the same option more then once, the last one will take
    455             // precedence. Also, all unsupported hardware options will be ignores.
    456             String cmd = "echo " + hwOption.getKey() + "=" + hwOption.getValue() + " >> "
    457                     + configFile.getAbsolutePath();
    458             CommandResult result = mRunUtil.runTimedCmd(getAvdTimeoutMS(), "sh", "-c", cmd);
    459             if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
    460                 CLog.d("Failed to add AVD hardware option '%s' stdout: '%s'", result.getStatus(),
    461                         result.getStdout());
    462                 // treat as TargetSetupError
    463                 throw new TargetSetupError(String.format(
    464                         "Unable to add hardware option to AVD. stderr: '%s'", result.getStderr()),
    465                         mTestDevice.getDeviceDescriptor());
    466             }
    467         }
    468     }
    469 
    470 
    471     /**
    472      * Launch emulator, performing multiple attempts if necessary as specified.
    473      *
    474      * @param device
    475      * @param avd
    476      * @param emulatorArgs
    477      * @throws BuildError
    478      */
    479     void launchEmulator(ITestDevice device, String avd, List<String> emulatorArgs)
    480             throws BuildError {
    481         for (int i = 1; i <= mLaunchAttempts; i++) {
    482             try {
    483                 getDeviceManager().launchEmulator(device, mMaxBootTime * 60 * 1000, mRunUtil,
    484                         emulatorArgs);
    485                 // hack alert! adb to emulator communication on first boot is notoriously flaky
    486                 // b/4644136
    487                 // send it a few adb commands to ensure the communication channel is stable
    488                 CLog.d("Testing adb to %s communication", device.getSerialNumber());
    489                 for (int j = 0; j < 3; j++) {
    490                     device.executeShellCommand("pm list instrumentation");
    491                     mRunUtil.sleep(2 * 1000);
    492                 }
    493 
    494                 // hurray - launched!
    495                 return;
    496             } catch (DeviceNotAvailableException e) {
    497                 CLog.w("Emulator for avd '%s' failed to launch on attempt %d of %d. Cause: %s",
    498                         avd, i, mLaunchAttempts, e);
    499             }
    500             try {
    501                 // ensure process has been killed
    502                 getDeviceManager().killEmulator(device);
    503             } catch (DeviceNotAvailableException e) {
    504                 // ignore
    505             }
    506         }
    507         throw new DeviceFailedToBootError(
    508                 String.format("Emulator for avd '%s' failed to boot.", avd),
    509                 device.getDeviceDescriptor());
    510     }
    511 
    512     /**
    513      * Sets the number of launch attempts to perform.
    514      *
    515      * @param launchAttempts
    516      */
    517     void setLaunchAttempts(int launchAttempts) {
    518         mLaunchAttempts = launchAttempts;
    519     }
    520 
    521     @Override
    522     public void cleanUp(IBuildInfo buildInfo, Throwable e) {
    523         if (mSdkHome != null) {
    524             CLog.i("Removing tmp sdk home dir %s", mSdkHome.getAbsolutePath());
    525             FileUtil.recursiveDelete(mSdkHome);
    526             mSdkHome = null;
    527         }
    528     }
    529 
    530     private IDeviceManager getDeviceManager() {
    531         if (mDeviceManager == null) {
    532             mDeviceManager = GlobalConfiguration.getDeviceManagerInstance();
    533         }
    534         return mDeviceManager;
    535     }
    536 
    537     private int getAvdTimeoutMS() {
    538         return mAvdTimeoutSeconds * 1000;
    539     }
    540 
    541     private void setCommandList(List<String> commands, String option, String value) {
    542         if (value != null) {
    543             commands.add(option);
    544             commands.add(value);
    545         }
    546     }
    547 }
    548