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