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