1 /*
2  * Copyright (C) 2021 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.bedstead.nene.utils;
18 
19 import static android.os.Build.VERSION_CODES.S;
20 
21 import android.app.UiAutomation;
22 import android.os.ParcelFileDescriptor;
23 import android.provider.Settings;
24 import android.util.Log;
25 
26 import androidx.test.platform.app.InstrumentationRegistry;
27 
28 import com.android.bedstead.nene.TestApis;
29 import com.android.bedstead.nene.exceptions.AdbException;
30 import com.android.compatibility.common.util.FileUtils;
31 
32 import java.io.FileInputStream;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.util.function.Function;
36 
37 /**
38  * Utilities for interacting with adb shell commands.
39  *
40  * <p>To enable command logging use the adb command `adb shell settings put global nene_log 1`.
41  */
42 public final class ShellCommandUtils {
43 
44     private static final String LOG_TAG = ShellCommandUtils.class.getName();
45 
46     private static final int OUT_DESCRIPTOR_INDEX = 0;
47     private static final int IN_DESCRIPTOR_INDEX = 1;
48     private static final int ERR_DESCRIPTOR_INDEX = 2;
49 
50     private static final TestApis sTestApis = new TestApis();
51 
52     private static final boolean SHOULD_LOG = shouldLog();
53 
shouldLog()54     private static boolean shouldLog() {
55         try {
56             return Settings.Global.getInt(
57                     sTestApis.context().instrumentedContext().getContentResolver(),
58                     "nene_log") == 1;
59         } catch (Settings.SettingNotFoundException e) {
60             return false;
61         }
62     }
63 
ShellCommandUtils()64     private ShellCommandUtils() { }
65 
66     /**
67      * Execute an adb shell command.
68      *
69      * <p>When running on S and above, any failures in executing the command will result in an
70      * {@link AdbException} being thrown. On earlier versions of Android, an {@link AdbException}
71      * will be thrown when the command returns no output (indicating that there is an error on
72      * stderr which cannot be read by this method) but some failures will return seemingly correctly
73      * but with an error in the returned string.
74      *
75      * <p>Callers should be careful to check the command's output is valid.
76      */
executeCommand(String command)77     static String executeCommand(String command) throws AdbException {
78         return executeCommand(command, /* allowEmptyOutput=*/ false, /* stdInBytes= */ null);
79     }
80 
executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)81     static String executeCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)
82             throws AdbException {
83         logCommand(command, allowEmptyOutput, stdInBytes);
84 
85         if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
86             return executeCommandPreS(command, allowEmptyOutput, stdInBytes);
87         }
88 
89         // TODO(scottjonathan): Add argument to force errors to stderr
90         try {
91 
92             ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command);
93             ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
94             ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
95             ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX];
96 
97             writeStdInAndClose(fdIn, stdInBytes);
98 
99             String out = new String(readStreamAndClose(fdOut));
100             String err = new String(readStreamAndClose(fdErr));
101 
102             if (!err.isEmpty()) {
103                 throw new AdbException("Error executing command", command, out, err);
104             }
105 
106             if (SHOULD_LOG) {
107                 Log.d(LOG_TAG, "Command result: " + out);
108             }
109 
110             return out;
111         } catch (IOException e) {
112             throw new AdbException("Error executing command", command, e);
113         }
114     }
115 
executeCommandForBytes(String command)116     static byte[] executeCommandForBytes(String command) throws AdbException {
117         return executeCommandForBytes(command, /* stdInBytes= */ null);
118     }
119 
executeCommandForBytes(String command, byte[] stdInBytes)120     static byte[] executeCommandForBytes(String command, byte[] stdInBytes) throws AdbException {
121         logCommand(command, /* allowEmptyOutput= */ false, stdInBytes);
122 
123         if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
124             return executeCommandForBytesPreS(command, stdInBytes);
125         }
126 
127         // TODO(scottjonathan): Add argument to force errors to stderr
128         try {
129 
130             ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRwe(command);
131             ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
132             ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
133             ParcelFileDescriptor fdErr = fds[ERR_DESCRIPTOR_INDEX];
134 
135             writeStdInAndClose(fdIn, stdInBytes);
136 
137             byte[] out = readStreamAndClose(fdOut);
138             String err = new String(readStreamAndClose(fdErr));
139 
140             if (!err.isEmpty()) {
141                 throw new AdbException("Error executing command", command, err);
142             }
143 
144             return out;
145         } catch (IOException e) {
146             throw new AdbException("Error executing command", command, e);
147         }
148     }
149 
logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes)150     private static void logCommand(String command, boolean allowEmptyOutput, byte[] stdInBytes) {
151         if (!SHOULD_LOG) {
152             return;
153         }
154 
155         StringBuilder logBuilder = new StringBuilder("Executing shell command ");
156         logBuilder.append(command);
157         if (allowEmptyOutput) {
158             logBuilder.append(" (allow empty output)");
159         }
160         if (stdInBytes != null) {
161             logBuilder.append(" (writing to stdIn)");
162         }
163         Log.d(LOG_TAG, logBuilder.toString());
164     }
165 
166     /**
167      * Execute an adb shell command and check that the output meets a given criteria.
168      *
169      * <p>On S and above, any output printed to standard error will result in an exception and the
170      * {@code outputSuccessChecker} not being called. Empty output will still be processed.
171      *
172      * <p>Prior to S, if there is no output on standard out, regardless of if there is output on
173      * standard error, {@code outputSuccessChecker} will not be called.
174      *
175      * <p>{@code outputSuccessChecker} should return {@code true} if the output indicates the
176      * command executed successfully.
177      */
executeCommandAndValidateOutput( String command, Function<String, Boolean> outputSuccessChecker)178     static String executeCommandAndValidateOutput(
179             String command, Function<String, Boolean> outputSuccessChecker) throws AdbException {
180         return executeCommandAndValidateOutput(command,
181                 /* allowEmptyOutput= */ false,
182                 /* stdInBytes= */ null,
183                 outputSuccessChecker);
184     }
185 
executeCommandAndValidateOutput( String command, boolean allowEmptyOutput, byte[] stdInBytes, Function<String, Boolean> outputSuccessChecker)186     static String executeCommandAndValidateOutput(
187             String command,
188             boolean allowEmptyOutput,
189             byte[] stdInBytes,
190             Function<String, Boolean> outputSuccessChecker) throws AdbException {
191         String output = executeCommand(command, allowEmptyOutput, stdInBytes);
192         if (!outputSuccessChecker.apply(output)) {
193             throw new AdbException("Command did not meet success criteria", command, output);
194         }
195         return output;
196     }
197 
198     /**
199      * Return {@code true} if {@code output} starts with "success", case insensitive.
200      */
startsWithSuccess(String output)201     public static boolean startsWithSuccess(String output) {
202         return output.toUpperCase().startsWith("SUCCESS");
203     }
204 
205     /**
206      * Return {@code true} if {@code output} does not start with "error", case insensitive.
207      */
doesNotStartWithError(String output)208     public static boolean doesNotStartWithError(String output) {
209         return !output.toUpperCase().startsWith("ERROR");
210     }
211 
executeCommandPreS( String command, boolean allowEmptyOutput, byte[] stdIn)212     private static String executeCommandPreS(
213             String command, boolean allowEmptyOutput, byte[] stdIn) throws AdbException {
214         ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command);
215         ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
216         ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
217 
218         try {
219             writeStdInAndClose(fdIn, stdIn);
220 
221             try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
222                 String out = new String(FileUtils.readInputStreamFully(fis));
223 
224                 if (!allowEmptyOutput && out.isEmpty()) {
225                     throw new AdbException(
226                             "No output from command. There's likely an error on stderr",
227                             command, out);
228                 }
229 
230                 if (SHOULD_LOG) {
231                     Log.d(LOG_TAG, "Command result: " + out);
232                 }
233 
234                 return out;
235             }
236         } catch (IOException e) {
237             throw new AdbException(
238                     "Error reading command output", command, e);
239         }
240     }
241 
242     // This is warned for executeShellCommandRw which did exist as TestApi
243     @SuppressWarnings("NewApi")
executeCommandForBytesPreS( String command, byte[] stdInBytes)244     private static byte[] executeCommandForBytesPreS(
245             String command, byte[] stdInBytes) throws AdbException {
246         ParcelFileDescriptor[] fds = uiAutomation().executeShellCommandRw(command);
247         ParcelFileDescriptor fdOut = fds[OUT_DESCRIPTOR_INDEX];
248         ParcelFileDescriptor fdIn = fds[IN_DESCRIPTOR_INDEX];
249 
250         try {
251             writeStdInAndClose(fdIn, stdInBytes);
252 
253             try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
254                 return FileUtils.readInputStreamFully(fis);
255             }
256         } catch (IOException e) {
257             throw new AdbException(
258                     "Error reading command output", command, e);
259         }
260     }
261 
writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)262     private static void writeStdInAndClose(ParcelFileDescriptor fdIn, byte[] stdInBytes)
263             throws IOException {
264         if (stdInBytes != null) {
265             try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(fdIn)) {
266                 fos.write(stdInBytes);
267             }
268         } else {
269             fdIn.close();
270         }
271     }
272 
readStreamAndClose(ParcelFileDescriptor fd)273     private static byte[] readStreamAndClose(ParcelFileDescriptor fd) throws IOException {
274         try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
275             return FileUtils.readInputStreamFully(fis);
276         }
277     }
278 
279     /**
280      * Get a {@link UiAutomation}.
281      */
uiAutomation()282     public static UiAutomation uiAutomation() {
283         return InstrumentationRegistry.getInstrumentation().getUiAutomation();
284     }
285 }
286