1 /* 2 * Copyright (C) 2019 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.device; 18 19 import com.android.ddmlib.IDevice; 20 import com.android.ddmlib.Log.LogLevel; 21 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey; 22 import com.android.tradefed.build.IBuildInfo; 23 import com.android.tradefed.device.cloud.GceAvdInfo; 24 import com.android.tradefed.log.ITestLogger; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.result.FileInputStreamSource; 27 import com.android.tradefed.result.ITestLoggerReceiver; 28 import com.android.tradefed.result.InputStreamSource; 29 import com.android.tradefed.result.LogDataType; 30 import com.android.tradefed.targetprep.TargetSetupError; 31 import com.android.tradefed.util.CommandResult; 32 import com.android.tradefed.util.CommandStatus; 33 import com.android.tradefed.util.FileUtil; 34 import com.android.tradefed.util.IRunUtil; 35 import com.android.tradefed.util.RunUtil; 36 import com.android.tradefed.util.TarUtil; 37 import com.android.tradefed.util.ZipUtil; 38 39 import com.google.common.annotations.VisibleForTesting; 40 import com.google.common.base.Strings; 41 import com.google.common.net.HostAndPort; 42 43 import java.io.File; 44 import java.io.IOException; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.List; 48 49 /** The class for local virtual devices running on TradeFed host. */ 50 public class LocalAndroidVirtualDevice extends RemoteAndroidDevice implements ITestLoggerReceiver { 51 52 private static final int INVALID_PORT = 0; 53 54 // Environment variables. 55 private static final String ANDROID_HOST_OUT = "ANDROID_HOST_OUT"; 56 private static final String TMPDIR = "TMPDIR"; 57 58 // The name of the GZIP file containing launch_cvd and stop_cvd. 59 private static final String CVD_HOST_PACKAGE_NAME = "cvd-host_package.tar.gz"; 60 61 private static final String ACLOUD_CVD_TEMP_DIR_NAME = "acloud_cvd_temp"; 62 private static final String CUTTLEFISH_RUNTIME_DIR_NAME = "cuttlefish_runtime"; 63 64 private ITestLogger mTestLogger = null; 65 66 // Temporary directories for images and tools. 67 private File mImageDir = null; 68 private File mHostPackageDir = null; 69 private List<File> mTempDirs = new ArrayList<File>(); 70 71 private GceAvdInfo mGceAvdInfo = null; 72 LocalAndroidVirtualDevice( IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)73 public LocalAndroidVirtualDevice( 74 IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) { 75 super(device, stateMonitor, allocationMonitor); 76 } 77 78 /** Execute common setup procedure and launch the virtual device. */ 79 @Override preInvocationSetup(IBuildInfo info)80 public void preInvocationSetup(IBuildInfo info) 81 throws TargetSetupError, DeviceNotAvailableException { 82 // The setup method in super class does not require the device to be online. 83 super.preInvocationSetup(info); 84 85 createTempDirs(info); 86 87 CommandResult result = null; 88 File report = null; 89 try { 90 report = FileUtil.createTempFile("report", ".json"); 91 result = acloudCreate(report, getOptions()); 92 loadAvdInfo(report); 93 } catch (IOException ex) { 94 throw new TargetSetupError( 95 "Cannot create acloud report file.", ex, getDeviceDescriptor()); 96 } finally { 97 FileUtil.deleteFile(report); 98 } 99 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 100 throw new TargetSetupError( 101 String.format("Cannot execute acloud command. stderr:\n%s", result.getStderr()), 102 getDeviceDescriptor()); 103 } 104 105 HostAndPort hostAndPort = mGceAvdInfo.hostAndPort(); 106 replaceStubDevice(hostAndPort.toString()); 107 108 RecoveryMode previousMode = getRecoveryMode(); 109 try { 110 setRecoveryMode(RecoveryMode.NONE); 111 if (!adbTcpConnect(hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) { 112 throw new TargetSetupError( 113 String.format("Cannot connect to %s.", hostAndPort), getDeviceDescriptor()); 114 } 115 waitForDeviceAvailable(); 116 } finally { 117 setRecoveryMode(previousMode); 118 } 119 } 120 121 /** Execute common tear-down procedure and stop the virtual device. */ 122 @Override postInvocationTearDown(Throwable exception)123 public void postInvocationTearDown(Throwable exception) { 124 TestDeviceOptions options = getOptions(); 125 HostAndPort hostAndPort = getHostAndPortFromAvdInfo(); 126 String instanceName = (mGceAvdInfo != null ? mGceAvdInfo.instanceName() : null); 127 try { 128 if (!options.shouldSkipTearDown() && hostAndPort != null) { 129 if (!adbTcpDisconnect( 130 hostAndPort.getHost(), Integer.toString(hostAndPort.getPort()))) { 131 CLog.e("Cannot disconnect from %s", hostAndPort.toString()); 132 } 133 } 134 135 if (!options.shouldSkipTearDown() && instanceName != null) { 136 CommandResult result = acloudDelete(instanceName, options); 137 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 138 CLog.e("Cannot stop the virtual device."); 139 } 140 } else { 141 CLog.i("Skip stopping the virtual device."); 142 } 143 144 if (instanceName != null) { 145 reportInstanceLogs(instanceName); 146 } 147 } finally { 148 restoreStubDevice(); 149 150 if (!options.shouldSkipTearDown()) { 151 deleteTempDirs(); 152 } else { 153 CLog.i( 154 "Skip deleting the temporary directories.\n" 155 + "Address: %s\nName: %s\nHost package: %s\nImage: %s", 156 hostAndPort, instanceName, mHostPackageDir, mImageDir); 157 mTempDirs.clear(); 158 mHostPackageDir = null; 159 mImageDir = null; 160 } 161 162 mGceAvdInfo = null; 163 164 super.postInvocationTearDown(exception); 165 } 166 } 167 168 @Override setTestLogger(ITestLogger testLogger)169 public void setTestLogger(ITestLogger testLogger) { 170 mTestLogger = testLogger; 171 } 172 173 /** 174 * Extract a file if the format is tar.gz or zip. 175 * 176 * @param file the file to be extracted. 177 * @return a temporary directory containing the extracted content if the file is an archive; 178 * otherwise return the input file. 179 * @throws IOException if the file cannot be extracted. 180 */ extractArchive(File file)181 private File extractArchive(File file) throws IOException { 182 if (file.isDirectory()) { 183 return file; 184 } 185 if (TarUtil.isGzip(file)) { 186 file = TarUtil.extractTarGzipToTemp(file, file.getName()); 187 mTempDirs.add(file); 188 } else if (ZipUtil.isZipFileValid(file, false)) { 189 file = ZipUtil.extractZipToTemp(file, file.getName()); 190 mTempDirs.add(file); 191 } else { 192 CLog.w("Cannot extract %s.", file); 193 } 194 return file; 195 } 196 197 /** Find host package in build info and extract to a temporary directory. */ findHostPackage(IBuildInfo buildInfo)198 private File findHostPackage(IBuildInfo buildInfo) throws TargetSetupError { 199 File hostPackageDir = null; 200 File hostPackage = buildInfo.getFile(CVD_HOST_PACKAGE_NAME); 201 if (hostPackage != null) { 202 try { 203 hostPackageDir = extractArchive(hostPackage); 204 } catch (IOException ex) { 205 throw new TargetSetupError( 206 "Cannot extract host package.", ex, getDeviceDescriptor()); 207 } 208 } 209 if (hostPackageDir == null) { 210 String androidHostOut = System.getenv(ANDROID_HOST_OUT); 211 if (!Strings.isNullOrEmpty(androidHostOut)) { 212 CLog.i( 213 "Use the host tools in %s as the build info does not provide host package.", 214 androidHostOut); 215 hostPackageDir = new File(androidHostOut); 216 } 217 } 218 if (hostPackageDir == null || !hostPackageDir.isDirectory()) { 219 throw new TargetSetupError( 220 String.format( 221 "Cannot find %s in build info and %s.", 222 CVD_HOST_PACKAGE_NAME, ANDROID_HOST_OUT), 223 getDeviceDescriptor()); 224 } 225 FileUtil.chmodRWXRecursively(new File(hostPackageDir, "bin")); 226 return hostPackageDir; 227 } 228 229 /** Find device images in build info and extract to a temporary directory. */ findDeviceImages(IBuildInfo buildInfo)230 private File findDeviceImages(IBuildInfo buildInfo) throws TargetSetupError { 231 File imageZip = buildInfo.getFile(BuildInfoFileKey.DEVICE_IMAGE); 232 if (imageZip == null) { 233 throw new TargetSetupError( 234 "Cannot find image zip in build info.", getDeviceDescriptor()); 235 } 236 try { 237 return extractArchive(imageZip); 238 } catch (IOException ex) { 239 throw new TargetSetupError("Cannot extract image zip.", ex, getDeviceDescriptor()); 240 } 241 } 242 243 /** Get the necessary files to create the instance. */ createTempDirs(IBuildInfo info)244 void createTempDirs(IBuildInfo info) throws TargetSetupError { 245 try { 246 mHostPackageDir = findHostPackage(info); 247 mImageDir = findDeviceImages(info); 248 } catch (TargetSetupError ex) { 249 deleteTempDirs(); 250 throw ex; 251 } 252 } 253 254 /** Delete all temporary directories. */ 255 @VisibleForTesting deleteTempDirs()256 void deleteTempDirs() { 257 for (File tempDir : mTempDirs) { 258 FileUtil.recursiveDelete(tempDir); 259 } 260 mTempDirs.clear(); 261 mImageDir = null; 262 mHostPackageDir = null; 263 } 264 265 /** 266 * Change the initial serial number of {@link StubLocalAndroidVirtualDevice}. 267 * 268 * @param newSerialNumber the serial number of the new stub device. 269 * @throws TargetSetupError if the original device type is not expected. 270 */ replaceStubDevice(String newSerialNumber)271 private void replaceStubDevice(String newSerialNumber) throws TargetSetupError { 272 IDevice device = getIDevice(); 273 if (!StubLocalAndroidVirtualDevice.class.equals(device.getClass())) { 274 throw new TargetSetupError( 275 "Unexpected device type: " + device.getClass(), getDeviceDescriptor()); 276 } 277 setIDevice(new StubLocalAndroidVirtualDevice(newSerialNumber)); 278 setFastbootEnabled(false); 279 } 280 281 /** Restore the {@link StubLocalAndroidVirtualDevice} with the initial serial number. */ restoreStubDevice()282 private void restoreStubDevice() { 283 setIDevice(new StubLocalAndroidVirtualDevice(getInitialSerial())); 284 setFastbootEnabled(false); 285 } 286 addLogLevelToAcloudCommand(List<String> command, LogLevel logLevel)287 private static void addLogLevelToAcloudCommand(List<String> command, LogLevel logLevel) { 288 if (LogLevel.VERBOSE.equals(logLevel)) { 289 command.add("-v"); 290 } else if (LogLevel.DEBUG.equals(logLevel)) { 291 command.add("-vv"); 292 } 293 } 294 acloudCreate(File report, TestDeviceOptions options)295 private CommandResult acloudCreate(File report, TestDeviceOptions options) { 296 CommandResult result = null; 297 298 File acloud = options.getAvdDriverBinary(); 299 if (acloud == null || !acloud.isFile()) { 300 CLog.e("Specified AVD driver binary is not a file."); 301 result = new CommandResult(CommandStatus.EXCEPTION); 302 result.setStderr("Specified AVD driver binary is not a file."); 303 return result; 304 } 305 acloud.setExecutable(true); 306 307 for (int attempt = 0; attempt < options.getGceMaxAttempt(); attempt++) { 308 result = 309 acloudCreate( 310 options.getGceCmdTimeout(), 311 acloud, 312 report, 313 options.getGceDriverLogLevel(), 314 options.getGceDriverParams()); 315 if (CommandStatus.SUCCESS.equals(result.getStatus())) { 316 break; 317 } 318 CLog.w( 319 "Failed to start local virtual instance with attempt: %d; command status: %s", 320 attempt, result.getStatus()); 321 } 322 return result; 323 } 324 acloudCreate( long timeout, File acloud, File report, LogLevel logLevel, List<String> args)325 private CommandResult acloudCreate( 326 long timeout, 327 File acloud, 328 File report, 329 LogLevel logLevel, 330 List<String> args) { 331 IRunUtil runUtil = createRunUtil(); 332 // The command creates the instance directory under TMPDIR. 333 runUtil.setEnvVariable(TMPDIR, getTmpDir().getAbsolutePath()); 334 335 List<String> command = 336 new ArrayList<String>( 337 Arrays.asList( 338 acloud.getAbsolutePath(), 339 "create", 340 "--local-instance", 341 "--local-image", 342 mImageDir.getAbsolutePath(), 343 "--local-tool", 344 mHostPackageDir.getAbsolutePath(), 345 "--report_file", 346 report.getAbsolutePath(), 347 "--no-autoconnect", 348 "--yes", 349 "--skip-pre-run-check")); 350 addLogLevelToAcloudCommand(command, logLevel); 351 command.addAll(args); 352 353 CommandResult result = runUtil.runTimedCmd(timeout, command.toArray(new String[0])); 354 CLog.i("acloud create stdout:\n%s", result.getStdout()); 355 CLog.i("acloud create stderr:\n%s", result.getStderr()); 356 return result; 357 } 358 359 /** 360 * Get valid host and port from mGceAvdInfo. 361 * 362 * @return {@link HostAndPort} if the port is valid; null otherwise. 363 */ getHostAndPortFromAvdInfo()364 private HostAndPort getHostAndPortFromAvdInfo() { 365 if (mGceAvdInfo == null) { 366 return null; 367 } 368 HostAndPort hostAndPort = mGceAvdInfo.hostAndPort(); 369 if (hostAndPort == null 370 || !hostAndPort.hasPort() 371 || hostAndPort.getPort() == INVALID_PORT) { 372 return null; 373 } 374 return hostAndPort; 375 } 376 377 /** Initialize instance name, host address, and port from an acloud report file. */ loadAvdInfo(File report)378 private void loadAvdInfo(File report) throws TargetSetupError { 379 mGceAvdInfo = GceAvdInfo.parseGceInfoFromFile(report, getDeviceDescriptor(), INVALID_PORT); 380 if (mGceAvdInfo == null) { 381 throw new TargetSetupError("Cannot read acloud report file.", getDeviceDescriptor()); 382 } 383 384 if (Strings.isNullOrEmpty(mGceAvdInfo.instanceName())) { 385 throw new TargetSetupError("No instance name in acloud report.", getDeviceDescriptor()); 386 } 387 388 if (getHostAndPortFromAvdInfo() == null) { 389 throw new TargetSetupError("No port in acloud report.", getDeviceDescriptor()); 390 } 391 392 if (!GceAvdInfo.GceStatus.SUCCESS.equals(mGceAvdInfo.getStatus())) { 393 throw new TargetSetupError( 394 "Cannot launch virtual device: " + mGceAvdInfo.getErrors(), 395 getDeviceDescriptor()); 396 } 397 } 398 acloudDelete(String instanceName, TestDeviceOptions options)399 private CommandResult acloudDelete(String instanceName, TestDeviceOptions options) { 400 File acloud = options.getAvdDriverBinary(); 401 if (acloud == null || !acloud.isFile()) { 402 CLog.e("Specified AVD driver binary is not a file."); 403 return new CommandResult(CommandStatus.EXCEPTION); 404 } 405 acloud.setExecutable(true); 406 407 IRunUtil runUtil = createRunUtil(); 408 runUtil.setEnvVariable(TMPDIR, getTmpDir().getAbsolutePath()); 409 410 List<String> command = 411 new ArrayList<String>( 412 Arrays.asList( 413 acloud.getAbsolutePath(), 414 "delete", 415 "--local-only", 416 "--instance-names", 417 instanceName)); 418 addLogLevelToAcloudCommand(command, options.getGceDriverLogLevel()); 419 420 CommandResult result = 421 runUtil.runTimedCmd(options.getGceCmdTimeout(), command.toArray(new String[0])); 422 CLog.i("acloud delete stdout:\n%s", result.getStdout()); 423 CLog.i("acloud delete stderr:\n%s", result.getStderr()); 424 return result; 425 } 426 reportInstanceLogs(String instanceName)427 private void reportInstanceLogs(String instanceName) { 428 if (mTestLogger == null) { 429 return; 430 } 431 File instanceDir = 432 FileUtil.getFileForPath( 433 getTmpDir(), 434 ACLOUD_CVD_TEMP_DIR_NAME, 435 instanceName, 436 CUTTLEFISH_RUNTIME_DIR_NAME); 437 reportInstanceLog(new File(instanceDir, "kernel.log"), LogDataType.KERNEL_LOG); 438 reportInstanceLog(new File(instanceDir, "logcat"), LogDataType.LOGCAT); 439 reportInstanceLog(new File(instanceDir, "launcher.log"), LogDataType.TEXT); 440 reportInstanceLog(new File(instanceDir, "cuttlefish_config.json"), LogDataType.TEXT); 441 } 442 reportInstanceLog(File file, LogDataType type)443 private void reportInstanceLog(File file, LogDataType type) { 444 if (file.exists()) { 445 try (InputStreamSource source = new FileInputStreamSource(file)) { 446 mTestLogger.testLog(file.getName(), type, source); 447 } 448 } else { 449 CLog.w("%s doesn't exist.", file.getAbsolutePath()); 450 } 451 } 452 453 @VisibleForTesting createRunUtil()454 IRunUtil createRunUtil() { 455 return new RunUtil(); 456 } 457 458 @VisibleForTesting getTmpDir()459 File getTmpDir() { 460 return new File(System.getProperty("java.io.tmpdir")); 461 } 462 } 463