1 /*
2  * Copyright (C) 2023 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.util;
18 
19 import com.android.tradefed.config.GlobalConfiguration;
20 import com.android.tradefed.error.HarnessException;
21 import com.android.tradefed.log.ITestLogger;
22 import com.android.tradefed.result.FileInputStreamSource;
23 import com.android.tradefed.result.InputStreamSource;
24 import com.android.tradefed.result.LogDataType;
25 import com.android.tradefed.result.error.InfraErrorIdentifier;
26 
27 import com.google.common.annotations.VisibleForTesting;
28 import com.google.common.base.Splitter;
29 
30 import java.io.File;
31 import java.io.IOException;
32 import java.time.Duration;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.List;
36 import java.util.Optional;
37 
38 import javax.annotation.Nullable;
39 
40 /** A Utility class to execute device actions. */
41 public class DeviceActionUtil {
42 
43     private static final String BUNDLETOOL_FLAG = "--da_bundletool";
44     private static final String CREDENTIAL_FLAG = "--da_cred_file";
45     private static final String ADB_FLAG = "--adb";
46     private static final String AAPT_FLAG = "--aapt";
47     private static final String JAVA_BIN_FLAG = "--java_command_path";
48     private static final String GEN_DIR_FLAG = "--da_gen_file_dir";
49     private static final String TMP_DIR_FLAG = "--tmp_dir_root";
50     private static final String GOOGLE_APPLICATION_CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS";
51     private static final String PATH = "PATH";
52     private static final String HOST_LOG = "host_log_";
53     private static final String DA_TMP_DIR = "da_tmp_dir";
54     private static final String DA_GEN_DIR = "da_gen_dir";
55     private static final String ANY_TXT_FILE_PATTERN = ".*\\.txt";
56     private static final long EXECUTION_TIMEOUT = Duration.ofHours(1).toMillis();
57 
58     private final File mDeviceActionMainJar;
59     private final String[] mConfigFlags;
60     private final IRunUtil mRunUtil;
61     private final File mGenDir;
62 
63     /** Commands for device action. */
64     public enum Command {
65         INSTALL_MAINLINE("install_mainline"),
66         RESET("reset");
67 
68         private final String cmdName;
69 
Command(String cmdName)70         Command(String cmdName) {
71             this.cmdName = cmdName;
72         }
73     }
74 
75     /** Exception for config error. */
76     public static class DeviceActionConfigError extends HarnessException {
77         private static final long serialVersionUID = 2202987086655357212L;
78 
DeviceActionConfigError(String message, @Nullable Throwable cause)79         public DeviceActionConfigError(String message, @Nullable Throwable cause) {
80             super(message, cause, InfraErrorIdentifier.INTERNAL_CONFIG_ERROR);
81         }
82 
DeviceActionConfigError(String message)83         public DeviceActionConfigError(String message) {
84             super(message, InfraErrorIdentifier.INTERNAL_CONFIG_ERROR);
85         }
86     }
87 
88     @VisibleForTesting
DeviceActionUtil( File deviceActionMainJar, String[] configFlags, IRunUtil runUtil, File genDir)89     DeviceActionUtil(
90             File deviceActionMainJar, String[] configFlags, IRunUtil runUtil, File genDir) {
91         mDeviceActionMainJar = deviceActionMainJar;
92         mConfigFlags = configFlags;
93         mRunUtil = runUtil;
94         mGenDir = genDir;
95     }
96 
97     /** Creates an instance. */
create(File deviceActionMainJar, File bundletoolJar)98     public static DeviceActionUtil create(File deviceActionMainJar, File bundletoolJar)
99             throws DeviceActionConfigError {
100         checkFile(deviceActionMainJar);
101         checkFile(bundletoolJar);
102         File genDir = createDir(DA_GEN_DIR);
103         File tmpDir = createDir(DA_TMP_DIR);
104         final String[] flags =
105                 createConfigFlags(
106                         bundletoolJar,
107                         getCredential(),
108                         genDir,
109                         tmpDir,
110                         getAdb(),
111                         getAapt(),
112                         SystemUtil.getRunningJavaBinaryPath());
113         return new DeviceActionUtil(deviceActionMainJar, flags, new RunUtil(), genDir);
114     }
115 
116     /**
117      * Executes a device action command.
118      *
119      * @param command to execute.
120      * @param deviceId of the device.
121      * @param actionArgs action args for the {@code command}.
122      */
execute(Command command, String deviceId, List<String> actionArgs)123     public CommandResult execute(Command command, String deviceId, List<String> actionArgs) {
124         return execute(command, createActionFlags(deviceId, actionArgs));
125     }
126 
127     /** Generates the host log file. */
generateLogFile(CommandResult result)128     public void generateLogFile(CommandResult result) throws IOException {
129         File logFile = FileUtil.createTempFile(HOST_LOG, ".txt", mGenDir);
130         FileUtil.writeToFile(result.getStdout(), logFile);
131         FileUtil.writeToFile(result.getStderr(), logFile, /* append= */ true);
132     }
133 
134     /** Saves all generated files to test logs. */
saveToLogs(Command cmd, ITestLogger testLogger)135     public void saveToLogs(Command cmd, ITestLogger testLogger) throws IOException {
136         for (String filePath : FileUtil.findFiles(mGenDir, ANY_TXT_FILE_PATTERN)) {
137             File file = new File(filePath);
138             try (InputStreamSource inputStreamSource = new FileInputStreamSource(file)) {
139                 String dataName =
140                         String.format("da_%s_%s", cmd.name(), file.getName().split("\\.", 2)[0]);
141                 testLogger.testLog(dataName, LogDataType.TEXT, inputStreamSource);
142             }
143         }
144     }
145 
146     /** Executes a device action command. */
execute(Command command, List<String> args)147     private CommandResult execute(Command command, List<String> args) {
148         ArrayList<String> cmd =
149                 new ArrayList<>(
150                         Arrays.asList(
151                                 SystemUtil.getRunningJavaBinaryPath().getAbsolutePath(),
152                                 "-Djava.util.logging.config.class=com.google.common.logging.GoogleConsoleLogConfig",
153                                 "-Dgoogle.debug_log_levels=*=INFO",
154                                 "-jar",
155                                 mDeviceActionMainJar.getAbsolutePath(),
156                                 command.cmdName));
157         cmd.addAll(Arrays.asList(mConfigFlags));
158         cmd.addAll(args);
159         return mRunUtil.runTimedCmd(EXECUTION_TIMEOUT, cmd.toArray(new String[0]));
160     }
161 
162     @VisibleForTesting
createConfigFlags( File bundletoolJar, File credential, File genDir, File tmpDir, File adb, File aapt, File java)163     static String[] createConfigFlags(
164             File bundletoolJar,
165             File credential,
166             File genDir,
167             File tmpDir,
168             File adb,
169             File aapt,
170             File java) {
171         return new String[] {
172             BUNDLETOOL_FLAG,
173             bundletoolJar.getAbsolutePath(),
174             CREDENTIAL_FLAG,
175             credential.getAbsolutePath(),
176             GEN_DIR_FLAG,
177             genDir.getAbsolutePath(),
178             TMP_DIR_FLAG,
179             tmpDir.getAbsolutePath(),
180             ADB_FLAG,
181             adb.getAbsolutePath(),
182             AAPT_FLAG,
183             aapt.getAbsolutePath(),
184             JAVA_BIN_FLAG,
185             java.getAbsolutePath()
186         };
187     }
188 
createActionFlags(String deviceId, List<String> args)189     private static List<String> createActionFlags(String deviceId, List<String> args) {
190         ArrayList<String> flags = new ArrayList<>(Arrays.asList("--device1", "serial=" + deviceId));
191         for (String arg : args) {
192             flags.addAll(Arrays.asList("--action", arg));
193         }
194         return flags;
195     }
196 
checkFile(File file)197     private static void checkFile(File file) throws DeviceActionConfigError {
198         if (file == null || !file.exists() || !file.isFile()) {
199             throw new DeviceActionConfigError("Missing file " + file);
200         }
201     }
202 
getAdb()203     private static File getAdb() throws DeviceActionConfigError {
204         File adbFile = new File(GlobalConfiguration.getDeviceManagerInstance().getAdbPath());
205         if (!adbFile.exists()) {
206             // Assume adb is in the PATH.
207             adbFile = findExecutableOnPath("adb");
208         }
209         return adbFile;
210     }
211 
getAapt()212     private static File getAapt() throws DeviceActionConfigError {
213         // Assume aapt2 is in the PATH. @see doc for {@link AaptParser}.
214         return findExecutableOnPath("aapt2");
215     }
216 
getCredential()217     private static File getCredential() throws DeviceActionConfigError {
218         File credentialFile = new File(System.getenv(GOOGLE_APPLICATION_CREDENTIALS));
219         return Optional.of(credentialFile)
220                 .filter(File::exists)
221                 .orElseThrow(
222                         () ->
223                                 new DeviceActionConfigError(
224                                         "Missing GOOGLE_APPLICATION_CREDENTIALS"));
225     }
226 
createDir(String prefix)227     private static File createDir(String prefix) throws DeviceActionConfigError {
228         try {
229             return FileUtil.createTempDir(prefix);
230         } catch (IOException e) {
231             throw new DeviceActionConfigError("Failed to create " + prefix, e);
232         }
233     }
234 
findExecutableOnPath(String name)235     private static File findExecutableOnPath(String name) throws DeviceActionConfigError {
236         return Splitter.on(File.pathSeparator).splitToList(System.getenv(PATH)).stream()
237                 .map(dir -> new File(dir, name))
238                 .filter(f -> f.isFile() && f.canExecute())
239                 .findFirst()
240                 .orElseThrow(
241                         () -> new DeviceActionConfigError("Failed to find the executable " + name));
242     }
243 }
244