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